-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Ethan Lee <[email protected]> Co-authored-by: Jiyoon Koo <[email protected]> Co-authored-by: Ethan Lee <[email protected]>
- Loading branch information
1 parent
5179dde
commit 82176c7
Showing
59 changed files
with
1,558 additions
and
2,154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
168 changes: 168 additions & 0 deletions
168
packages/web5/lib/src/dids/did_dht/converters/did_document_converter.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.