Skip to content

Commit

Permalink
Refactor did:dht (#68)
Browse files Browse the repository at this point in the history
Co-authored-by: Ethan Lee <[email protected]>
Co-authored-by: Jiyoon Koo <[email protected]>
Co-authored-by: Ethan Lee <[email protected]>
  • Loading branch information
4 people authored Apr 25, 2024
1 parent 5179dde commit 82176c7
Show file tree
Hide file tree
Showing 59 changed files with 1,558 additions and 2,154 deletions.
12 changes: 10 additions & 2 deletions packages/web5/lib/src/crypto/secp256k1.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,15 @@ class Secp256k1 {
}

static Jwk bytesToPublicKey(Uint8List input) {
// TODO: implement bytesToPublicKey
throw UnimplementedError();
final xBytes = input.sublist(1, 33);
final yBytes = input.sublist(33, 65);

return Jwk(
kty: kty,
alg: alg,
crv: crv,
x: Base64Url.encode(xBytes),
y: Base64Url.encode(yBytes),
);
}
}
12 changes: 6 additions & 6 deletions packages/web5/lib/src/dids/did_core/did_document.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class DidDocument implements DidResource {
/// A DID controller is an entity that is authorized to make changes to a
/// DID document. The process of authorizing a DID controller is defined
/// by the DID method.
final dynamic controller; // String or List<String>
final List<String>? controller;

/// cryptographic public keys, which can be used to authenticate or authorize
/// interactions with the DID subject or associated parties.
Expand Down Expand Up @@ -83,8 +83,8 @@ class DidDocument implements DidResource {
List<String>? capabilityInvocation;

DidDocument({
this.context,
required this.id,
this.context,
this.alsoKnownAs,
this.controller,
this.verificationMethod,
Expand All @@ -98,13 +98,13 @@ class DidDocument implements DidResource {

void addVerificationMethod(
DidVerificationMethod vm, {
VerificationPurpose? purpose,
List<VerificationPurpose> purpose = const [],
}) {
verificationMethod ??= [];
verificationMethod!.add(vm);

if (purpose != null) {
addVerificationPurpose(purpose, vm.id);
for (final p in purpose) {
addVerificationPurpose(p, vm.id);
}
}

Expand Down Expand Up @@ -234,7 +234,7 @@ class DidDocument implements DidResource {
context: json['context'],
id: json['id'],
alsoKnownAs: json['alsoKnownAs']?.cast<String>(),
controller: json['controller'],
controller: json['controller']?.cast<List<String>>(),
verificationMethod: (json['verificationMethod'] as List<dynamic>?)
?.map((item) => DidVerificationMethod.fromJson(item))
.toList(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:web5/src/dids/did_dht/registered_did_type.dart';
import 'package:web5/src/dids/did_dht/registered_types.dart';

/// contains metadata about the DID document contained in the didDocument
/// property. This metadata typically does not change between invocations of
Expand Down
43 changes: 43 additions & 0 deletions packages/web5/lib/src/dids/did_dht/bencoder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:convert';
import 'dart:typed_data';

enum Token {
dict('d', 100),
integer('i', 105),
list('l', 108),
end('e', 101);

final String value;
final int byte;

const Token(this.value, this.byte);
}

/// More information about Bencode can be found
/// [here](https://wiki.theory.org/BitTorrentSpecification#Bencoding)
class Bencoder {
// Encodes various Dart types into Bencoded format
static String bencode(dynamic input) {
if (input is String) {
return '${input.length}:$input';
} else if (input is int) {
return '${Token.integer.value}$input${Token.end.value}';
} else if (input is Uint8List) {
final str = utf8.decode(input);
return bencode(str);
} else {
throw FormatException('Unsupported type: ${input.runtimeType}');
}
}

static Uint8List encode(dynamic input) {
if (input is String || input is int) {
return utf8.encode(bencode(input));
} else if (input is Uint8List) {
final prefix = utf8.encode('${input.length}:');
return Uint8List.fromList([...prefix, ...input]);
} else {
throw FormatException('Unsupported type: ${input.runtimeType}');
}
}
}
101 changes: 101 additions & 0 deletions packages/web5/lib/src/dids/did_dht/bep44.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'dart:typed_data';
import 'package:web5/src/crypto.dart';
import 'package:web5/src/dids/did_dht/bencoder.dart';

typedef Signer = Future<Uint8List> Function(Uint8List payload);

/// Represents a BEP44 message, which is used for storing and retrieving data
/// in the Mainline DHT network.
///
/// A BEP44 message is used primarily in the context of the DID DHT method
/// for publishing and resolving DID documents in the DHT network. This type
/// encapsulates the data structure required for such operations in accordance
/// with BEP44.
///
/// See [BEP44 Specification](https://www.bittorrent.org/beps/bep_0044.html)
class Bep44Message {
static Future<Uint8List> create(
Uint8List message,
int seq,
Signer sign,
) async {
final toSign = BytesBuilder(copy: false);
toSign.add(Bencoder.encode('seq'));
toSign.add(Bencoder.encode(seq));
toSign.add(Bencoder.encode('v'));
toSign.add(Bencoder.encode(message));

final sig = await sign(toSign.toBytes());

// The sequence number needs to be converted to a big-endian byte array.
final seqBytes = ByteData(8)..setInt64(0, seq, Endian.big);
final encoded = BytesBuilder(copy: false);

encoded.add(sig);
encoded.add(seqBytes.buffer.asUint8List());
encoded.add(message);

return encoded.toBytes();
}

static DecodedBep44Message decode(Uint8List bytes) {
if (bytes.length < 72) {
throw FormatException(
'Response must be at least 72 bytes but got: ${bytes.length}',
);
}

if (bytes.length > 1072) {
throw FormatException(
'Response is larger than 1072 bytes, got: ${bytes.length}',
);
}

final sig = bytes.sublist(0, 64);
final seqBytes = ByteData.sublistView(bytes, 64, 72);
final int seq = seqBytes.getUint64(0, Endian.big);
final v = bytes.sublist(72);

// The public key 'k' is not provided in the data and needs to be handled accordingly.
return DecodedBep44Message(seq: seq, sig: sig, v: v);
}

static DecodedBep44Message verify(Uint8List input, Uint8List publicKey) {
final message = decode(input);
message.verify(publicKey);

return message;
}
}

class DecodedBep44Message {
/// The sequence number of the message, used to ensure the latest version of
/// the data is retrieved and updated. It's a monotonically increasing number.
int seq;

/// The signature of the message, ensuring the authenticity and integrity
/// of the data. It's computed over the bencoded sequence number and value.
Uint8List sig;

/// The actual data being stored or retrieved from the DHT network, typically
/// encoded in a format suitable for DNS packet representation of a DID Document.
Uint8List v;

DecodedBep44Message({
required this.seq,
required this.sig,
required this.v,
});

void verify(Uint8List publicKey) async {
final toSign = BytesBuilder(copy: false);
toSign.add(Bencoder.encode('seq'));
toSign.add(Bencoder.encode(seq));
toSign.add(Bencoder.encode('v'));
toSign.add(Bencoder.encode(v));

final jwk = Crypto.bytesToPublicKey(AlgorithmId.ed25519, publicKey);

await Ed25519.verify(jwk, toSign.toBytes(), sig);
}
}
3 changes: 3 additions & 0 deletions packages/web5/lib/src/dids/did_dht/converters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export './converters/did_document_converter.dart';
export './converters/vm_converter.dart';
export './converters/service_converter.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'package:web5/src/dids/did.dart';
import 'package:web5/src/dids/did_core.dart';
import 'package:web5/src/dids/did_dht/dns_packet.dart';
import 'package:web5/src/dids/did_dht/root_record.dart';
import 'package:web5/src/dids/did_dht/converters/vm_converter.dart';
import 'package:web5/src/dids/did_dht/converters/service_converter.dart';

/// Class that houses methods to convert a [DidDocument] to a [DnsPacket]
/// and vice versa.
class DidDocumentConverter {
/// Converts a [DidDocument] to a [DnsPacket].
static DnsPacket convertDidDocument(DidDocument document) {
final rootRecord = RootRecord();
final List<Answer<TxtData>> answers = [];

final vmRecordMap = {};

final verificationMethods = document.verificationMethod ?? [];
for (var i = 0; i < verificationMethods.length; i++) {
final vm = verificationMethods[i];
final txtRecord =
VerificationMethodConverter.convertVerificationMethod(i, vm);

answers.add(txtRecord);
rootRecord.addVmRecordName(i);

vmRecordMap[vm.id] = i;
}

final assertionMethods = document.assertionMethod ?? [];
for (final am in assertionMethods) {
final vmRecordName = vmRecordMap[am];
if (vmRecordName != null) {
rootRecord.addAsmRecordName(vmRecordName);
}
}

final authMethods = document.authentication ?? [];
for (final am in authMethods) {
final vmRecordName = vmRecordMap[am];
if (vmRecordName != null) {
rootRecord.addAuthRecordName(vmRecordName);
}
}

final capabilityDelegations = document.capabilityDelegation ?? [];
for (final cd in capabilityDelegations) {
final vmRecordName = vmRecordMap[cd];
if (vmRecordName != null) {
rootRecord.addDelRecordName(vmRecordName);
}
}

final capabilityInvocations = document.capabilityInvocation ?? [];
for (final ci in capabilityInvocations) {
final vmRecordName = vmRecordMap[ci];
if (vmRecordName != null) {
rootRecord.addInvRecordName(vmRecordName);
}
}

final agmMethods = document.keyAgreement ?? [];
for (final agm in agmMethods) {
final vmRecordName = vmRecordMap[agm];
if (vmRecordName != null) {
rootRecord.addAgmRecordName(vmRecordName);
}
}

final serviceRecords = document.service ?? [];
for (var i = 0; i < serviceRecords.length; i++) {
final service = serviceRecords[i];
final txtRecord = ServiceRecordConverter.convertService(i, service);

answers.add(txtRecord);
rootRecord.addSvcRecordName(i);
}

answers.insert(0, rootRecord.toTxtRecord(document.id));

return DnsPacket.create(answers);
}

static DidDocument convertDnsPacket(Did did, DnsPacket dnsPacket) {
final didDocument = DidDocument(id: did.uri);

final purposesMap = {};
RootRecord? rootRecord;

for (final answer in dnsPacket.answers) {
if (answer.type != RecordType.TXT) {
continue;
}

// lame but necessary. can't use as Answer<TxtData> because in Dart,
// even though TxtData is a subtype of RData, Answer<TxtData>
// is not considered a subtype of Answer<RData> because generic types are
//invariant. This means that even if B is a subtype of A, Generic<B>
// is not considered a subtype of Generic<A>
final txtData = answer.data as TxtData;
final txtRecord = Answer<TxtData>(
name: answer.name,
type: answer.type,
klass: answer.klass,
ttl: answer.ttl,
data: txtData,
);

if (answer.name.value == '_did.${did.id}') {
rootRecord = RootRecord.fromTxtRecord(txtRecord);
} else if (txtRecord.name.value.startsWith('_k')) {
final vm =
VerificationMethodConverter.convertTxtRecord(did.uri, txtRecord);
didDocument.addVerificationMethod(vm);

final delim = txtRecord.name.value.indexOf('.', 3);
final recordName = txtRecord.name.value.substring(1, delim);
purposesMap[recordName] = vm.id;
} else if (txtRecord.name.value.startsWith('_s')) {
final service =
ServiceRecordConverter.convertTxtRecord(did.uri, txtRecord);
didDocument.addService(service);
}
}

for (final recordName in rootRecord!.asm) {
final vmId = purposesMap[recordName];
didDocument.addVerificationPurpose(
VerificationPurpose.assertionMethod,
vmId,
);
}

for (final recordName in rootRecord.auth) {
final vmId = purposesMap[recordName];
didDocument.addVerificationPurpose(
VerificationPurpose.authentication,
vmId,
);
}

for (final recordName in rootRecord.del) {
final vmId = purposesMap[recordName];
didDocument.addVerificationPurpose(
VerificationPurpose.capabilityDelegation,
vmId,
);
}

for (final recordName in rootRecord.inv) {
final vmId = purposesMap[recordName];
didDocument.addVerificationPurpose(
VerificationPurpose.capabilityInvocation,
vmId,
);
}

for (final recordName in rootRecord.agm) {
final vmId = purposesMap[recordName];
didDocument.addVerificationPurpose(
VerificationPurpose.keyAgreement,
vmId,
);
}

return didDocument;
}
}
Loading

0 comments on commit 82176c7

Please sign in to comment.