From d2d96eebee68d4a7da4f9add28a90ca45b2ba2b6 Mon Sep 17 00:00:00 2001 From: gabe Date: Mon, 9 Oct 2023 16:35:14 -0700 Subject: [PATCH] merge --- packages/dids/.mocharc.json | 6 +- packages/dids/src/dht.ts | 593 ++++++++++++++-------------- packages/dids/src/did-dht.ts | 10 +- packages/dids/tests/dht.spec.ts | 2 +- packages/dids/tests/did-dht.spec.ts | 1 - 5 files changed, 304 insertions(+), 308 deletions(-) diff --git a/packages/dids/.mocharc.json b/packages/dids/.mocharc.json index 4fb9938fd..ab67dfc38 100644 --- a/packages/dids/.mocharc.json +++ b/packages/dids/.mocharc.json @@ -1,9 +1,5 @@ { "enable-source-maps": true, "exit": true, - "spec": ["tests/compiled/**/*.spec.js"], - "node-option": [ - "experimental-specifier-resolution=node", - "loader=ts-node/esm" - ] + "spec": ["tests/compiled/**/*.spec.js"] } diff --git a/packages/dids/src/dht.ts b/packages/dids/src/dht.ts index 39ef55fa1..b3e2c0958 100644 --- a/packages/dids/src/dht.ts +++ b/packages/dids/src/dht.ts @@ -1,19 +1,20 @@ -import {Jose, PublicKeyJwk, Web5Crypto} from '@web5/crypto'; import DHT from 'bittorrent-dht'; import ed from 'bittorrent-dht-sodium'; import type {DidDocument} from './types.js'; import dns, {AUTHORITATIVE_ANSWER, Packet, TxtAnswer} from 'dns-packet'; import Encoder from '@decentralized-identity/ion-sdk/dist/lib/Encoder.js'; +import type {PublicKeyJwk, Web5Crypto} from '@web5/crypto'; +import {Jose} from '@web5/crypto'; const DEFAULT_BOOTSTRAP = [ - 'router.magnets.im:6881', - 'router.bittorrent.com:6881', - 'router.utorrent.com:6881', - 'dht.transmissionbt.com:6881', - 'router.nuh.dev:6881' + 'router.magnets.im:6881', + 'router.bittorrent.com:6881', + 'router.utorrent.com:6881', + 'dht.transmissionbt.com:6881', + 'router.nuh.dev:6881' ].map(addr => { - const [host, port] = addr.split(':'); - return {host, port: Number(port)}; + const [host, port] = addr.split(':'); + return {host, port: Number(port)}; }); const TTL = 7200; @@ -30,310 +31,310 @@ export type PutRequest = { }; export class DidDht { - private dht: DHT; - - constructor() { - this.dht = new DHT({bootstrap: DEFAULT_BOOTSTRAP, verify: ed.verify}); - - this.dht.listen(0, () => { - console.debug('DHT is listening...'); - }); - } - - public async createPutDidRequest(keypair: Web5Crypto.CryptoKeyPair, did: DidDocument): Promise { - const seq = Math.ceil(Date.now() / 1000); - const v = await DidDht.toEncodedDnsPacket(did); - return { - seq : seq, - v : Buffer.from(v), - k : Buffer.from(keypair.publicKey.material), - sk : Buffer.concat([keypair.privateKey.material, keypair.publicKey.material]) - }; - } - - public async put(request: PutRequest): Promise { - const opts = { - k : request.k, - v : request.v, - seq : request.seq, - sign : function (buf: Buffer) { - return ed.sign(buf, request.sk); - } - }; - return new Promise((resolve, reject) => { - this.dht.put(opts, (err, hash) => { - if (err) { - reject(err); - } else { - resolve(hash.toString('hex')); - } - }); - }); - } - - public async parseGetDidResponse(id: string, response: Buffer): Promise { - return await DidDht.fromEncodedDnsPacket(id, response); - } - - public async get(keyHash: string): Promise { - return new Promise((resolve, reject) => { - this.dht.get(keyHash, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res.v); - } - }); - }); - } - - public static async toEncodedDnsPacket(document: DidDocument): Promise { - const packet: Partial = { - id : 0, - type : 'response', - flags : AUTHORITATIVE_ANSWER, - answers : [] - }; - - const rootRecord: string[] = []; - const keyLookup = new Map(); - - // Add key records for each verification method - for (const vm of document.verificationMethod) { - const index = document.verificationMethod.indexOf(vm); - const recordIdentifier = `k${index}`; - let vmId = DidDht.identifierFragment(vm.id); - keyLookup.set(vmId, recordIdentifier); - - let keyType: number; - switch (vm.publicKeyJwk.alg) { - case 'EdDSA': - keyType = 0; - break; - case 'ES256K': - keyType = 1; - break; - default: - keyType = 0; // Default value or throw an error if needed - } - - const cryptoKey = await Jose.jwkToCryptoKey({key: vm.publicKeyJwk}); - const keyBase64Url = Encoder.encode(cryptoKey.material); - const keyRecord: TxtAnswer = { - type : 'TXT', - name : `_${recordIdentifier}._did`, - ttl : TTL, - data : Buffer.from(`id=${vmId},t=${keyType},k=${keyBase64Url}`) - }; - - packet.answers.push(keyRecord); - rootRecord.push(`vm=${recordIdentifier}`); - } + private dht: DHT; - // Add service records - document.service?.forEach((service, index) => { - const recordIdentifier = `s${index}`; - let sId = DidDht.identifierFragment(service.id); - const serviceRecord: TxtAnswer = { - type : 'TXT', - name : `_${recordIdentifier}._did`, - ttl : TTL, - data : Buffer.from(`id=${sId},t=${service.type},uri=${service.serviceEndpoint}`) - }; - - packet.answers.push(serviceRecord); - rootRecord.push(`srv=${recordIdentifier}`); - }); - - // add verification relationships - if (document.authentication) { - const authIds: string[] = document.authentication - .map(id => DidDht.identifierFragment(id)) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`auth=${authIds.join(',')}`); - } + constructor() { + this.dht = new DHT({bootstrap: DEFAULT_BOOTSTRAP, verify: ed.verify}); + + this.dht.listen(0, () => { + console.debug('DHT is listening...'); + }); } - if (document.assertionMethod) { - const authIds: string[] = document.assertionMethod - .map(id => DidDht.identifierFragment(id)) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`asm=${authIds.join(',')}`); - } + + public async createPutDidRequest(keypair: Web5Crypto.CryptoKeyPair, did: DidDocument): Promise { + const seq = Math.ceil(Date.now() / 1000); + const v = await DidDht.toEncodedDnsPacket(did); + return { + seq: seq, + v: Buffer.from(v), + k: Buffer.from(keypair.publicKey.material), + sk: Buffer.concat([keypair.privateKey.material, keypair.publicKey.material]) + }; } - if (document.keyAgreement) { - const authIds: string[] = document.keyAgreement - .map(id => DidDht.identifierFragment(id)) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`agm=${authIds.join(',')}`); - } + + public async put(request: PutRequest): Promise { + const opts = { + k: request.k, + v: request.v, + seq: request.seq, + sign: function (buf: Buffer) { + return ed.sign(buf, request.sk); + } + }; + return new Promise((resolve, reject) => { + this.dht.put(opts, (err, hash) => { + if (err) { + reject(err); + } else { + resolve(hash.toString('hex')); + } + }); + }); } - if (document.capabilityInvocation) { - const authIds: string[] = document.capabilityInvocation - .map(id => DidDht.identifierFragment(id)) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`inv=${authIds.join(',')}`); - } + + public async parseGetDidResponse(id: string, response: Buffer): Promise { + return await DidDht.fromEncodedDnsPacket(id, response); } - if (document.capabilityDelegation) { - const authIds: string[] = document.capabilityDelegation - .map(id => DidDht.identifierFragment(id)) - .filter(id => keyLookup.has(id)) - .map(id => keyLookup.get(id) as string); - if (authIds.length) { - rootRecord.push(`del=${authIds.join(',')}`); - } + + public async get(keyHash: string): Promise { + return new Promise((resolve, reject) => { + this.dht.get(keyHash, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res.v); + } + }); + }); } - // Add root record - packet.answers.push({ - type : 'TXT', - name : '_did', - ttl : TTL, - data : Buffer.from(rootRecord.join(';')) - }); + public static async toEncodedDnsPacket(document: DidDocument): Promise { + const packet: Partial = { + id: 0, + type: 'response', + flags: AUTHORITATIVE_ANSWER, + answers: [] + }; + + const rootRecord: string[] = []; + const keyLookup = new Map(); + + // Add key records for each verification method + for (const vm of document.verificationMethod) { + const index = document.verificationMethod.indexOf(vm); + const recordIdentifier = `k${index}`; + let vmId = DidDht.identifierFragment(vm.id); + keyLookup.set(vmId, recordIdentifier); + + let keyType: number; + switch (vm.publicKeyJwk.alg) { + case 'EdDSA': + keyType = 0; + break; + case 'ES256K': + keyType = 1; + break; + default: + keyType = 0; // Default value or throw an error if needed + } + + const cryptoKey = await Jose.jwkToCryptoKey({key: vm.publicKeyJwk}); + const keyBase64Url = Encoder.encode(cryptoKey.material); + const keyRecord: TxtAnswer = { + type: 'TXT', + name: `_${recordIdentifier}._did`, + ttl: TTL, + data: Buffer.from(`id=${vmId},t=${keyType},k=${keyBase64Url}`) + }; + + packet.answers.push(keyRecord); + rootRecord.push(`vm=${recordIdentifier}`); + } + + // Add service records + document.service?.forEach((service, index) => { + const recordIdentifier = `s${index}`; + let sId = DidDht.identifierFragment(service.id); + const serviceRecord: TxtAnswer = { + type: 'TXT', + name: `_${recordIdentifier}._did`, + ttl: TTL, + data: Buffer.from(`id=${sId},t=${service.type},uri=${service.serviceEndpoint}`) + }; + + packet.answers.push(serviceRecord); + rootRecord.push(`srv=${recordIdentifier}`); + }); + + // add verification relationships + if (document.authentication) { + const authIds: string[] = document.authentication + .map(id => DidDht.identifierFragment(id)) + .filter(id => keyLookup.has(id)) + .map(id => keyLookup.get(id) as string); + if (authIds.length) { + rootRecord.push(`auth=${authIds.join(',')}`); + } + } + if (document.assertionMethod) { + const authIds: string[] = document.assertionMethod + .map(id => DidDht.identifierFragment(id)) + .filter(id => keyLookup.has(id)) + .map(id => keyLookup.get(id) as string); + if (authIds.length) { + rootRecord.push(`asm=${authIds.join(',')}`); + } + } + if (document.keyAgreement) { + const authIds: string[] = document.keyAgreement + .map(id => DidDht.identifierFragment(id)) + .filter(id => keyLookup.has(id)) + .map(id => keyLookup.get(id) as string); + if (authIds.length) { + rootRecord.push(`agm=${authIds.join(',')}`); + } + } + if (document.capabilityInvocation) { + const authIds: string[] = document.capabilityInvocation + .map(id => DidDht.identifierFragment(id)) + .filter(id => keyLookup.has(id)) + .map(id => keyLookup.get(id) as string); + if (authIds.length) { + rootRecord.push(`inv=${authIds.join(',')}`); + } + } + if (document.capabilityDelegation) { + const authIds: string[] = document.capabilityDelegation + .map(id => DidDht.identifierFragment(id)) + .filter(id => keyLookup.has(id)) + .map(id => keyLookup.get(id) as string); + if (authIds.length) { + rootRecord.push(`del=${authIds.join(',')}`); + } + } + + // Add root record + packet.answers.push({ + type: 'TXT', + name: '_did', + ttl: TTL, + data: Buffer.from(rootRecord.join(';')) + }); - return dns.encode(packet); - } + return dns.encode(packet); + } - private static identifierFragment(identifier: string): string { - return identifier.includes('#') ? identifier.substring(identifier.indexOf('#') + 1) : identifier; - } + private static identifierFragment(identifier: string): string { + return identifier.includes('#') ? identifier.substring(identifier.indexOf('#') + 1) : identifier; + } - /** + /** * @param did The DID of the document * @param encodedPacket A Uint8Array containing the encoded DNS packet */ - public static async fromEncodedDnsPacket(did: string, encodedPacket: Buffer): Promise { - const packet = dns.decode(encodedPacket); - const document: Partial = { - id: did, - }; - - const keyLookup = new Map(); - - for (const answer of packet.answers) { - if (answer.type !== 'TXT') continue; - - const dataStr = answer.data?.toString(); - // Extracts 'k' or 's' from "_k0._did" or "_s0._did" - const recordType = answer.name?.split('.')[0].substring(1, 2); - - /*eslint-disable no-case-declarations*/ - switch (recordType) { - case 'k': - const {id, t, k} = DidDht.parseTxtData(dataStr); - const keyConfigurations: { [keyType: string]: Partial } = { - '0': { - crv : 'Ed25519', - kty : 'OKP', - alg : 'EdDSA' - }, - '1': { - crv : 'secp256k1', - kty : 'EC', - alg : 'ES256K' + public static async fromEncodedDnsPacket(did: string, encodedPacket: Buffer): Promise { + const packet = dns.decode(encodedPacket); + const document: Partial = { + id: did, + }; + + const keyLookup = new Map(); + + for (const answer of packet.answers) { + if (answer.type !== 'TXT') continue; + + const dataStr = answer.data?.toString(); + // Extracts 'k' or 's' from "_k0._did" or "_s0._did" + const recordType = answer.name?.split('.')[0].substring(1, 2); + + /*eslint-disable no-case-declarations*/ + switch (recordType) { + case 'k': + const {id, t, k} = DidDht.parseTxtData(dataStr); + const keyConfigurations: { [keyType: string]: Partial } = { + '0': { + crv: 'Ed25519', + kty: 'OKP', + alg: 'EdDSA' + }, + '1': { + crv: 'secp256k1', + kty: 'EC', + alg: 'ES256K' + } + }; + const keyConfig = keyConfigurations[t]; + if (!keyConfig) { + throw new Error('Unsupported key type'); + } + + const publicKeyJwk = await Jose.keyToJwk({ + ...keyConfig, + kid: id, + keyMaterial: Encoder.decodeAsBytes(k, 'key'), + keyType: 'public' + }) as PublicKeyJwk; + + if (!document.verificationMethod) { + document.verificationMethod = []; + } + document.verificationMethod.push({ + id: `${did}#${id}`, + type: 'JsonWebKey2020', + controller: did, + publicKeyJwk: publicKeyJwk, + }); + keyLookup.set(answer.name, id); + break; + case 's': + const {id: sId, t: sType, uri} = DidDht.parseTxtData(dataStr); + + if (!document.service) { + document.service = []; + } + document.service.push({ + id: `${did}#${sId}`, + type: sType, + serviceEndpoint: uri + }); + break; + } + } + + // Extract relationships from root record + const root = packet.answers.filter(answer => answer.name === '_did'); + if (!root.length) { + throw new Error('No root record found'); + } + if (root.length > 1) { + throw new Error('Multiple root records found'); + } + const singleRoot = root[0] as dns.TxtAnswer; + const rootRecord = singleRoot.data?.toString().split(';'); + rootRecord?.forEach(record => { + const [type, ids] = record.split('='); + const idList = ids?.split(',').map(id => `#${keyLookup.get(`_${id}._did`)}`); + switch (type) { + case 'auth': + document.authentication = idList; + break; + case 'asm': + document.assertionMethod = idList; + break; + case 'agm': + document.keyAgreement = idList; + break; + case 'inv': + document.capabilityInvocation = idList; + break; + case 'del': + document.capabilityDelegation = idList; + break; } - }; - const keyConfig = keyConfigurations[t]; - if (!keyConfig) { - throw new Error('Unsupported key type'); - } - - const publicKeyJwk = await Jose.keyToJwk({ - ...keyConfig, - kid : id, - keyMaterial : Encoder.decodeAsBytes(k, 'key'), - keyType : 'public' - }) as PublicKeyJwk; - - if (!document.verificationMethod) { - document.verificationMethod = []; - } - document.verificationMethod.push({ - id : `${did}#${id}`, - type : 'JsonWebKey2020', - controller : did, - publicKeyJwk : publicKeyJwk, - }); - keyLookup.set(answer.name, id); - break; - case 's': - const {id: sId, t: sType, uri} = DidDht.parseTxtData(dataStr); - - if (!document.service) { - document.service = []; - } - document.service.push({ - id : `${did}#${sId}`, - type : sType, - serviceEndpoint : uri - }); - break; - } + }); + + return document as DidDocument; } - // Extract relationships from root record - const root = packet.answers.filter(answer => answer.name === '_did'); - if (!root.length) { - throw new Error('No root record found'); + public static parseTxtData(data: string): { [key: string]: string } { + return data.split(',').reduce((acc, pair) => { + const [key, value] = pair.split('='); + acc[key] = value; + return acc; + }, {} as { [key: string]: string }); } - if (root.length > 1) { - throw new Error('Multiple root records found'); + + public static async printEncodedDnsPacket(encodedPacket: Buffer) { + const packet = dns.decode(encodedPacket); + packet.answers.forEach(answer => { + if (answer.type === 'TXT') { + const data = answer.data.toString(); + console.log(answer.name, data); + } + }); + } + + public destroy(): void { + this.dht.destroy(); } - const singleRoot = root[0] as dns.TxtAnswer; - const rootRecord = singleRoot.data?.toString().split(';'); - rootRecord?.forEach(record => { - const [type, ids] = record.split('='); - const idList = ids?.split(',').map(id => `#${keyLookup.get(`_${id}._did`)}`); - switch (type) { - case 'auth': - document.authentication = idList; - break; - case 'asm': - document.assertionMethod = idList; - break; - case 'agm': - document.keyAgreement = idList; - break; - case 'inv': - document.capabilityInvocation = idList; - break; - case 'del': - document.capabilityDelegation = idList; - break; - } - }); - - return document as DidDocument; - } - - public static parseTxtData(data: string): { [key: string]: string } { - return data.split(',').reduce((acc, pair) => { - const [key, value] = pair.split('='); - acc[key] = value; - return acc; - }, {} as { [key: string]: string }); - } - - public static async printEncodedDnsPacket(encodedPacket: Buffer) { - const packet = dns.decode(encodedPacket); - packet.answers.forEach(answer => { - if (answer.type === 'TXT') { - const data = answer.data.toString(); - console.log(answer.name, data); - } - }); - } - - public destroy(): void { - this.dht.destroy(); - } } diff --git a/packages/dids/src/did-dht.ts b/packages/dids/src/did-dht.ts index b2df93ec8..f3806d603 100644 --- a/packages/dids/src/did-dht.ts +++ b/packages/dids/src/did-dht.ts @@ -1,5 +1,6 @@ import z32 from 'z32'; -import crypto from 'crypto'; +import { sha1 } from '@noble/hashes/sha1'; +import {EcdsaAlgorithm, EdDsaAlgorithm, Jose, JwkKeyPair, PublicKeyJwk, Web5Crypto} from '@web5/crypto'; import { DidDocument, DidKeySetVerificationMethodKey, @@ -8,7 +9,6 @@ import { DidService, PortableDid, VerificationRelationship } from './types.js'; -import {EcdsaAlgorithm, EdDsaAlgorithm, Jose, JwkKeyPair, PublicKeyJwk, Web5Crypto} from '@web5/crypto'; import {DidDht} from './dht.js'; const SupportedCryptoKeyTypes = [ @@ -109,12 +109,12 @@ export class DidDhtMethod implements DidMethod { const hash = did.replace('did:dht:', ''); const decoded = z32.decode(hash); const identifier = this.hash(decoded); - const retrievedValue = await dht.get(identifier.toString('hex')); + const retrievedValue = await dht.get(Buffer.from(identifier).toString('hex')); return await dht.parseGetDidResponse(did, retrievedValue).finally(() => dht.destroy()); } - private static hash(input: crypto.BinaryLike) { - return crypto.createHash('sha1').update(input).digest(); + private static hash(input: Uint8Array) { + return sha1(input); } public static async getDidIdentifier(options: { diff --git a/packages/dids/tests/dht.spec.ts b/packages/dids/tests/dht.spec.ts index cca286e2e..8cdd94286 100644 --- a/packages/dids/tests/dht.spec.ts +++ b/packages/dids/tests/dht.spec.ts @@ -1,7 +1,7 @@ import {expect} from 'chai'; +import {Jose} from '@web5/crypto'; import {DidDht} from '../src/dht.js'; import {DidDhtKeySet, DidDhtMethod} from '../src/did-dht.js'; -import {Jose} from '@web5/crypto'; import {DidKeySetVerificationMethodKey, DidService} from '../src/index.js'; describe('DHT', function () { diff --git a/packages/dids/tests/did-dht.spec.ts b/packages/dids/tests/did-dht.spec.ts index 277179bd3..192f0cb53 100644 --- a/packages/dids/tests/did-dht.spec.ts +++ b/packages/dids/tests/did-dht.spec.ts @@ -212,7 +212,6 @@ describe('did-dht', () => { expect(didResolutionResult.didDocument.service).to.not.exist; const gotDid = await DidDhtMethod.resolve(document.id); - console.log('gotDid', gotDid); expect(gotDid.id).to.deep.equal(document.id); expect(gotDid.service).to.deep.equal(document.service); expect(gotDid.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id);