diff --git a/packages/core/package.json b/packages/core/package.json index 8a8a83fce8..da4e667d75 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,7 +26,7 @@ "@multiformats/base-x": "^4.0.1", "@stablelib/ed25519": "^1.0.2", "@stablelib/sha256": "^1.0.1", - "@types/indy-sdk": "^1.16.8", + "@types/indy-sdk": "^1.16.12", "@types/node-fetch": "^2.5.10", "@types/ws": "^7.4.4", "abort-controller": "^3.0.0", diff --git a/packages/core/src/modules/credentials/services/CredentialService.ts b/packages/core/src/modules/credentials/services/CredentialService.ts index 8e6b6265d1..1a39489138 100644 --- a/packages/core/src/modules/credentials/services/CredentialService.ts +++ b/packages/core/src/modules/credentials/services/CredentialService.ts @@ -648,11 +648,21 @@ export class CredentialService { const credentialDefinition = await this.ledgerService.getCredentialDefinition(indyCredential.cred_def_id) + //Fetch Revocation Registry Definition if the issued credential has an associated revocation registry id + let revocationRegistryDefinition + if (indyCredential.rev_reg_id) { + const revocationRegistryDefinitionData = await this.ledgerService.getRevocationRegistryDefinition( + indyCredential.rev_reg_id + ) + revocationRegistryDefinition = revocationRegistryDefinitionData.revocationRegistryDefinition + } + const credentialId = await this.indyHolderService.storeCredential({ credentialId: uuid(), credentialRequestMetadata, credential: indyCredential, credentialDefinition, + revocationRegistryDefinition, }) credentialRecord.credentialId = credentialId credentialRecord.credentialMessage = issueCredentialMessage diff --git a/packages/core/src/modules/indy/services/IndyHolderService.ts b/packages/core/src/modules/indy/services/IndyHolderService.ts index 7dab13a57f..bcb0e1fdb1 100644 --- a/packages/core/src/modules/indy/services/IndyHolderService.ts +++ b/packages/core/src/modules/indy/services/IndyHolderService.ts @@ -1,41 +1,78 @@ +import type { Logger } from '../../../logger' +import type { RequestedCredentials } from '../../proofs' import type * as Indy from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' -import { IndySdkError } from '../../../error' +import { IndySdkError } from '../../../error/IndySdkError' import { isIndyError } from '../../../utils/indyError' import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyRevocationService } from './IndyRevocationService' + @scoped(Lifecycle.ContainerScoped) export class IndyHolderService { private indy: typeof Indy + private logger: Logger private wallet: IndyWallet + private indyRevocationService: IndyRevocationService - public constructor(agentConfig: AgentConfig, wallet: IndyWallet) { + public constructor(agentConfig: AgentConfig, indyRevocationService: IndyRevocationService, wallet: IndyWallet) { this.indy = agentConfig.agentDependencies.indy this.wallet = wallet + this.indyRevocationService = indyRevocationService + this.logger = agentConfig.logger } + /** + * Creates an Indy Proof in response to a proof request. Will create revocation state if the proof request requests proof of non-revocation + * + * @param proofRequest a Indy proof request + * @param requestedCredentials the requested credentials to use for the proof creation + * @param schemas schemas to use in proof creation + * @param credentialDefinitions credential definitions to use in proof creation + * @throws {Error} if there is an error during proof generation or revocation state generation + * @returns a promise of Indy Proof + * + * @todo support attribute non_revoked fields + */ public async createProof({ proofRequest, requestedCredentials, schemas, credentialDefinitions, - revocationStates = {}, - }: CreateProofOptions) { + }: CreateProofOptions): Promise { try { - return await this.indy.proverCreateProof( + this.logger.debug('Creating Indy Proof') + const revocationStates: Indy.RevStates = await this.indyRevocationService.createRevocationState( + proofRequest, + requestedCredentials + ) + + const indyProof: Indy.IndyProof = await this.indy.proverCreateProof( this.wallet.handle, proofRequest, - requestedCredentials, + requestedCredentials.toJSON(), this.wallet.masterSecretId, schemas, credentialDefinitions, revocationStates ) + + this.logger.trace('Created Indy Proof', { + indyProof, + }) + + return indyProof } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error creating Indy Proof`, { + error, + proofRequest, + requestedCredentials, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -49,7 +86,7 @@ export class IndyHolderService { credential, credentialDefinition, credentialId, - revocationRegistryDefinitions, + revocationRegistryDefinition, }: StoreCredentialOptions): Promise { try { return await this.indy.proverStoreCredential( @@ -58,10 +95,14 @@ export class IndyHolderService { credentialRequestMetadata, credential, credentialDefinition, - revocationRegistryDefinitions ?? null + revocationRegistryDefinition ?? null ) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error storing Indy Credential '${credentialId}'`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -78,7 +119,11 @@ export class IndyHolderService { try { return await this.indy.proverGetCredential(this.wallet.handle, credentialId) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error getting Indy Credential '${credentialId}'`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -101,7 +146,12 @@ export class IndyHolderService { this.wallet.masterSecretId ) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error creating Indy Credential Request`, { + error, + credentialOffer, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -177,7 +227,11 @@ export class IndyHolderService { return credentials } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error Fetching Indy Credentials For Referent`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } } @@ -201,13 +255,12 @@ export interface StoreCredentialOptions { credential: Indy.Cred credentialDefinition: Indy.CredDef credentialId?: Indy.CredentialId - revocationRegistryDefinitions?: Indy.RevRegsDefs + revocationRegistryDefinition?: Indy.RevocRegDef } export interface CreateProofOptions { proofRequest: Indy.IndyProofRequest - requestedCredentials: Indy.IndyRequestedCredentials + requestedCredentials: RequestedCredentials schemas: Indy.Schemas credentialDefinitions: Indy.CredentialDefs - revocationStates?: Indy.RevStates } diff --git a/packages/core/src/modules/indy/services/IndyIssuerService.ts b/packages/core/src/modules/indy/services/IndyIssuerService.ts index 011790247d..5658e76272 100644 --- a/packages/core/src/modules/indy/services/IndyIssuerService.ts +++ b/packages/core/src/modules/indy/services/IndyIssuerService.ts @@ -9,7 +9,6 @@ import type { CredReq, CredRevocId, CredValues, - BlobReaderHandle, } from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' @@ -18,18 +17,21 @@ import { AgentConfig } from '../../../agent/AgentConfig' import { AriesFrameworkError } from '../../../error/AriesFrameworkError' import { IndySdkError } from '../../../error/IndySdkError' import { isIndyError } from '../../../utils/indyError' -import { getDirFromFilePath } from '../../../utils/path' import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyUtilitiesService } from './IndyUtilitiesService' + @scoped(Lifecycle.ContainerScoped) export class IndyIssuerService { private indy: typeof Indy private wallet: IndyWallet + private indyUtilitiesService: IndyUtilitiesService private fileSystem: FileSystem - public constructor(agentConfig: AgentConfig, wallet: IndyWallet) { + public constructor(agentConfig: AgentConfig, wallet: IndyWallet, indyUtilitiesService: IndyUtilitiesService) { this.indy = agentConfig.agentDependencies.indy this.wallet = wallet + this.indyUtilitiesService = indyUtilitiesService this.fileSystem = agentConfig.fileSystem } @@ -44,7 +46,7 @@ export class IndyIssuerService { return schema } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -74,7 +76,7 @@ export class IndyIssuerService { return credentialDefinition } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -88,7 +90,7 @@ export class IndyIssuerService { try { return await this.indy.issuerCreateCredentialOffer(this.wallet.handle, credentialDefinitionId) } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -106,7 +108,7 @@ export class IndyIssuerService { }: CreateCredentialOptions): Promise<[Cred, CredRevocId]> { try { // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await this.createTailsReader(tailsFilePath) : 0 + const tailsReaderHandle = tailsFilePath ? await this.indyUtilitiesService.createTailsReader(tailsFilePath) : 0 if (revocationRegistryId || tailsFilePath) { throw new AriesFrameworkError('Revocation not supported yet') @@ -123,42 +125,7 @@ export class IndyIssuerService { return [credential, credentialRevocationId] } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } - } - - /** - * Get a handler for the blob storage tails file reader. - * - * @param tailsFilePath The path of the tails file - * @returns The blob storage reader handle - */ - private async createTailsReader(tailsFilePath: string): Promise { - try { - const tailsFileExists = await this.fileSystem.exists(tailsFilePath) - - // Extract directory from path (should also work with windows paths) - const dirname = getDirFromFilePath(tailsFilePath) - - if (!tailsFileExists) { - throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) - } - - const tailsReaderConfig = { - base_dir: dirname, - } - - return await this.indy.openBlobStorageReader('default', tailsReaderConfig) - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error + throw isIndyError(error) ? new IndySdkError(error) : error } } } diff --git a/packages/core/src/modules/indy/services/IndyRevocationService.ts b/packages/core/src/modules/indy/services/IndyRevocationService.ts new file mode 100644 index 0000000000..0edf9078c1 --- /dev/null +++ b/packages/core/src/modules/indy/services/IndyRevocationService.ts @@ -0,0 +1,199 @@ +import type { Logger } from '../../../logger' +import type { FileSystem } from '../../../storage/FileSystem' +import type { RevocationInterval } from '../../credentials/models/RevocationInterval' +import type { RequestedCredentials } from '../../proofs' +import type { default as Indy } from 'indy-sdk' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { IndySdkError } from '../../../error/IndySdkError' +import { isIndyError } from '../../../utils/indyError' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyLedgerService } from '../../ledger' + +import { IndyUtilitiesService } from './IndyUtilitiesService' + +enum RequestReferentType { + Attribute = 'attribute', + Predicate = 'predicate', + SelfAttestedAttribute = 'self-attested-attribute', +} + +@scoped(Lifecycle.ContainerScoped) +export class IndyRevocationService { + private indy: typeof Indy + private indyUtilitiesService: IndyUtilitiesService + private fileSystem: FileSystem + private ledgerService: IndyLedgerService + private logger: Logger + private wallet: IndyWallet + + public constructor( + agentConfig: AgentConfig, + indyUtilitiesService: IndyUtilitiesService, + ledgerService: IndyLedgerService, + wallet: IndyWallet + ) { + this.fileSystem = agentConfig.fileSystem + this.indy = agentConfig.agentDependencies.indy + this.indyUtilitiesService = indyUtilitiesService + this.logger = agentConfig.logger + this.ledgerService = ledgerService + this.wallet = wallet + } + + public async createRevocationState( + proofRequest: Indy.IndyProofRequest, + requestedCredentials: RequestedCredentials + ): Promise { + try { + this.logger.debug(`Creating Revocation State(s) for proof request`, { + proofRequest, + requestedCredentials, + }) + const revocationStates: Indy.RevStates = {} + const referentCredentials = [] + + //Retrieve information for referents and push to single array + for (const [referent, requestedCredential] of Object.entries(requestedCredentials.requestedAttributes)) { + referentCredentials.push({ + referent, + credentialInfo: requestedCredential.credentialInfo, + type: RequestReferentType.Attribute, + }) + } + for (const [referent, requestedCredential] of Object.entries(requestedCredentials.requestedPredicates)) { + referentCredentials.push({ + referent, + credentialInfo: requestedCredential.credentialInfo, + type: RequestReferentType.Predicate, + }) + } + + for (const { referent, credentialInfo, type } of referentCredentials) { + if (!credentialInfo) { + throw new AriesFrameworkError( + `Credential for referent '${referent} does not have credential info for revocation state creation` + ) + } + + // Prefer referent-specific revocation interval over global revocation interval + const referentRevocationInterval = + type === RequestReferentType.Predicate + ? proofRequest.requested_predicates[referent].non_revoked + : proofRequest.requested_attributes[referent].non_revoked + const requestRevocationInterval = referentRevocationInterval ?? proofRequest.non_revoked + const credentialRevocationId = credentialInfo.credentialRevocationId + const revocationRegistryId = credentialInfo.revocationRegistryId + + // If revocation interval is present and the credential is revocable then create revocation state + if (requestRevocationInterval && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation for ${type} referent '${referent}', creating revocation state for credential`, + { + requestRevocationInterval, + credentialRevocationId, + revocationRegistryId, + } + ) + + this.assertRevocationInterval(requestRevocationInterval) + + const { revocationRegistryDefinition } = await this.ledgerService.getRevocationRegistryDefinition( + revocationRegistryId + ) + + const { revocationRegistryDelta, deltaTimestamp } = await this.ledgerService.getRevocationRegistryDelta( + revocationRegistryId, + requestRevocationInterval?.to, + 0 + ) + + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) + + const revocationState = await this.indy.createRevocationState( + tails, + revocationRegistryDefinition, + revocationRegistryDelta, + deltaTimestamp, + credentialRevocationId + ) + const timestamp = revocationState.timestamp + + if (!revocationStates[revocationRegistryId]) { + revocationStates[revocationRegistryId] = {} + } + revocationStates[revocationRegistryId][timestamp] = revocationState + } + } + + this.logger.debug(`Created Revocation States for Proof Request`, { + revocationStates, + }) + + return revocationStates + } catch (error) { + this.logger.error(`Error creating Indy Revocation State for Proof Request`, { + error, + proofRequest, + requestedCredentials, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + + // Get revocation status for credential (given a from-to) + // Note from-to interval details: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + public async getRevocationStatus( + credentialRevocationId: string, + revocationRegistryDefinitionId: string, + requestRevocationInterval: RevocationInterval + ): Promise<{ revoked: boolean; deltaTimestamp: number }> { + this.logger.trace( + `Fetching Credential Revocation Status for Credential Revocation Id '${credentialRevocationId}' with revocation interval with to '${requestRevocationInterval.to}' & from '${requestRevocationInterval.from}'` + ) + + this.assertRevocationInterval(requestRevocationInterval) + + const { revocationRegistryDelta, deltaTimestamp } = await this.ledgerService.getRevocationRegistryDelta( + revocationRegistryDefinitionId, + requestRevocationInterval.to, + 0 + ) + + const revoked: boolean = revocationRegistryDelta.value.revoked?.includes(parseInt(credentialRevocationId)) || false + this.logger.trace( + `Credental with Credential Revocation Id '${credentialRevocationId}' is ${ + revoked ? '' : 'not ' + }revoked with revocation interval with to '${requestRevocationInterval.to}' & from '${ + requestRevocationInterval.from + }'` + ) + + return { + revoked, + deltaTimestamp, + } + } + + // TODO: Add Test + // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints + private assertRevocationInterval(requestRevocationInterval: RevocationInterval) { + if (!requestRevocationInterval.to) { + throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) + } + + if ( + (requestRevocationInterval.from || requestRevocationInterval.from === 0) && + requestRevocationInterval.to !== requestRevocationInterval.from + ) { + throw new AriesFrameworkError( + `Presentation requests proof of non-revocation with an interval from: '${requestRevocationInterval.from}' that does not match the interval to: '${requestRevocationInterval.to}', as specified in Aries RFC 0441` + ) + } + } +} diff --git a/packages/core/src/modules/indy/services/IndyUtilitiesService.ts b/packages/core/src/modules/indy/services/IndyUtilitiesService.ts new file mode 100644 index 0000000000..96b8ec4407 --- /dev/null +++ b/packages/core/src/modules/indy/services/IndyUtilitiesService.ts @@ -0,0 +1,84 @@ +import type { Logger } from '../../../logger' +import type { FileSystem } from '../../../storage/FileSystem' +import type { default as Indy, BlobReaderHandle } from 'indy-sdk' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { AriesFrameworkError } from '../../../error' +import { IndySdkError } from '../../../error/IndySdkError' +import { isIndyError } from '../../../utils/indyError' +import { getDirFromFilePath } from '../../../utils/path' + +@scoped(Lifecycle.ContainerScoped) +export class IndyUtilitiesService { + private indy: typeof Indy + private logger: Logger + private fileSystem: FileSystem + + public constructor(agentConfig: AgentConfig) { + this.indy = agentConfig.agentDependencies.indy + this.logger = agentConfig.logger + this.fileSystem = agentConfig.fileSystem + } + + /** + * Get a handler for the blob storage tails file reader. + * + * @param tailsFilePath The path of the tails file + * @returns The blob storage reader handle + */ + public async createTailsReader(tailsFilePath: string): Promise { + try { + this.logger.debug(`Opening tails reader at path ${tailsFilePath}`) + const tailsFileExists = await this.fileSystem.exists(tailsFilePath) + + // Extract directory from path (should also work with windows paths) + const dirname = getDirFromFilePath(tailsFilePath) + + if (!tailsFileExists) { + throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) + } + + const tailsReaderConfig = { + base_dir: dirname, + } + + const tailsReader = await this.indy.openBlobStorageReader('default', tailsReaderConfig) + this.logger.debug(`Opened tails reader at path ${tailsFilePath}`) + return tailsReader + } catch (error) { + if (isIndyError(error)) { + throw new IndySdkError(error) + } + + throw error + } + } + + public async downloadTails(hash: string, tailsLocation: string): Promise { + try { + this.logger.debug(`Checking to see if tails file for URL ${tailsLocation} has been stored in the FileSystem`) + const filePath = `${this.fileSystem.basePath}/afj/tails/${hash}` + + const tailsExists = await this.fileSystem.exists(filePath) + this.logger.debug(`Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${filePath}`) + if (!tailsExists) { + this.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + + await this.fileSystem.downloadToFile(tailsLocation, filePath) + this.logger.debug(`Saved tails file to FileSystem at path ${filePath}`) + + //TODO: Validate Tails File Hash + } + + this.logger.debug(`Tails file for URL ${tailsLocation} is stored in the FileSystem, opening tails reader`) + return this.createTailsReader(filePath) + } catch (error) { + this.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw isIndyError(error) ? new IndySdkError(error) : error + } + } +} diff --git a/packages/core/src/modules/indy/services/IndyVerifierService.ts b/packages/core/src/modules/indy/services/IndyVerifierService.ts index 6197767c58..8e0522357c 100644 --- a/packages/core/src/modules/indy/services/IndyVerifierService.ts +++ b/packages/core/src/modules/indy/services/IndyVerifierService.ts @@ -4,13 +4,17 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { IndySdkError } from '../../../error' +import { isIndyError } from '../../../utils/indyError' +import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' @scoped(Lifecycle.ContainerScoped) export class IndyVerifierService { private indy: typeof Indy + private ledgerService: IndyLedgerService - public constructor(agentConfig: AgentConfig) { + public constructor(agentConfig: AgentConfig, ledgerService: IndyLedgerService) { this.indy = agentConfig.agentDependencies.indy + this.ledgerService = ledgerService } public async verifyProof({ @@ -18,21 +22,48 @@ export class IndyVerifierService { proof, schemas, credentialDefinitions, - revocationRegistryDefinitions = {}, - revocationStates = {}, }: VerifyProofOptions): Promise { try { + const { revocationRegistryDefinitions, revocationRegistryStates } = await this.getRevocationRegistries(proof) + return await this.indy.verifierVerifyProof( proofRequest, proof, schemas, credentialDefinitions, revocationRegistryDefinitions, - revocationStates + revocationRegistryStates ) } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + + private async getRevocationRegistries(proof: Indy.IndyProof) { + const revocationRegistryDefinitions: Indy.RevocRegDefs = {} + const revocationRegistryStates: Indy.RevStates = Object.create(null) + for (const identifier of proof.identifiers) { + const revocationRegistryId = identifier.rev_reg_id + const timestamp = identifier.timestamp + + //Fetch Revocation Registry Definition if not already fetched + if (revocationRegistryId && !revocationRegistryDefinitions[revocationRegistryId]) { + const { revocationRegistryDefinition } = await this.ledgerService.getRevocationRegistryDefinition( + revocationRegistryId + ) + revocationRegistryDefinitions[revocationRegistryId] = revocationRegistryDefinition + } + + //Fetch Revocation Registry by Timestamp if not already fetched + if (revocationRegistryId && timestamp && !revocationRegistryStates[revocationRegistryId]?.[timestamp]) { + if (!revocationRegistryStates[revocationRegistryId]) { + revocationRegistryStates[revocationRegistryId] = Object.create(null) + } + const { revocationRegistry } = await this.ledgerService.getRevocationRegistry(revocationRegistryId, timestamp) + revocationRegistryStates[revocationRegistryId][timestamp] = revocationRegistry + } } + return { revocationRegistryDefinitions, revocationRegistryStates } } } @@ -41,6 +72,4 @@ export interface VerifyProofOptions { proof: Indy.IndyProof schemas: Indy.Schemas credentialDefinitions: Indy.CredentialDefs - revocationRegistryDefinitions?: Indy.RevRegsDefs - revocationStates?: Indy.RevStates } diff --git a/packages/core/src/modules/indy/services/index.ts b/packages/core/src/modules/indy/services/index.ts index 1fbeefc66f..fa01eaf419 100644 --- a/packages/core/src/modules/indy/services/index.ts +++ b/packages/core/src/modules/indy/services/index.ts @@ -1,3 +1,5 @@ export * from './IndyHolderService' export * from './IndyIssuerService' export * from './IndyVerifierService' +export * from './IndyUtilitiesService' +export * from './IndyRevocationService' diff --git a/packages/core/src/modules/ledger/LedgerModule.ts b/packages/core/src/modules/ledger/LedgerModule.ts index b004858c52..d89d76b219 100644 --- a/packages/core/src/modules/ledger/LedgerModule.ts +++ b/packages/core/src/modules/ledger/LedgerModule.ts @@ -72,4 +72,16 @@ export class LedgerModule { public async getCredentialDefinition(id: string) { return this.ledgerService.getCredentialDefinition(id) } + + public async getRevocationRegistryDefinition(revocationRegistryDefinitionId: string) { + return this.ledgerService.getRevocationRegistryDefinition(revocationRegistryDefinitionId) + } + + public async getRevocationRegistryDelta( + revocationRegistryDefinitionId: string, + fromSeconds = 0, + toSeconds = new Date().getTime() + ) { + return this.ledgerService.getRevocationRegistryDelta(revocationRegistryDefinitionId, fromSeconds, toSeconds) + } } diff --git a/packages/core/src/modules/ledger/services/IndyLedgerService.ts b/packages/core/src/modules/ledger/services/IndyLedgerService.ts index e50092ce77..eb31b8b1d5 100644 --- a/packages/core/src/modules/ledger/services/IndyLedgerService.ts +++ b/packages/core/src/modules/ledger/services/IndyLedgerService.ts @@ -14,10 +14,14 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { IndySdkError } from '../../../error/IndySdkError' -import { didFromCredentialDefinitionId, didFromSchemaId } from '../../../utils/did' +import { + didFromSchemaId, + didFromCredentialDefinitionId, + didFromRevocationRegistryDefinitionId, +} from '../../../utils/did' import { isIndyError } from '../../../utils/indyError' import { IndyWallet } from '../../../wallet/IndyWallet' -import { IndyIssuerService } from '../../indy' +import { IndyIssuerService } from '../../indy/services/IndyIssuerService' import { IndyPoolService } from './IndyPoolService' @@ -154,16 +158,19 @@ export class IndyLedgerService { const { pool } = await this.indyPoolService.getPoolForDid(did) try { - this.logger.debug(`Get schema '${schemaId}' from ledger '${pool.id}'`) + this.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.id}'`) const request = await this.indy.buildGetSchemaRequest(null, schemaId) - this.logger.debug(`Submitting get schema request for schema '${schemaId}' to ledger '${pool.id}'`) + this.logger.trace(`Submitting get schema request for schema '${schemaId}' to ledger '${pool.id}'`) const response = await this.submitReadRequest(pool, request) + this.logger.trace(`Got un-parsed schema '${schemaId}' from ledger '${pool.id}'`, { + response, + }) + const [, schema] = await this.indy.parseGetSchemaResponse(response) this.logger.debug(`Got schema '${schemaId}' from ledger '${pool.id}'`, { - response, schema, }) @@ -186,7 +193,7 @@ export class IndyLedgerService { try { this.logger.debug( - `Register credential definition on ledger '${pool.id}' with did '${did}'`, + `Registering credential definition on ledger '${pool.id}' with did '${did}'`, credentialDefinitionTemplate ) const { schema, tag, signatureType, supportRevocation } = credentialDefinitionTemplate @@ -230,19 +237,19 @@ export class IndyLedgerService { this.logger.debug(`Using ledger '${pool.id}' to retrieve credential definition '${credentialDefinitionId}'`) try { - this.logger.debug(`Get credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`) - const request = await this.indy.buildGetCredDefRequest(null, credentialDefinitionId) - this.logger.debug( + this.logger.trace( `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.id}'` ) const response = await this.submitReadRequest(pool, request) + this.logger.trace(`Got un-parsed credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`, { + response, + }) const [, credentialDefinition] = await this.indy.parseGetCredDefResponse(response) this.logger.debug(`Got credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`, { - response, credentialDefinition, }) @@ -258,6 +265,157 @@ export class IndyLedgerService { } } + public async getRevocationRegistryDefinition( + revocationRegistryDefinitionId: string + ): Promise { + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` + ) + try { + //TODO - implement a cache + this.logger.trace( + `Revocation Registry Definition '${revocationRegistryDefinitionId}' not cached, retrieving from ledger` + ) + + const request = await this.indy.buildGetRevocRegDefRequest(null, revocationRegistryDefinitionId) + + this.logger.trace( + `Submitting get revocation registry definition request for revocation registry definition '${revocationRegistryDefinitionId}' to ledger` + ) + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got un-parsed revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.id}'`, + { + response, + } + ) + + const [, revocationRegistryDefinition] = await this.indy.parseGetRevocRegDefResponse(response) + + this.logger.debug(`Got revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, { + revocationRegistryDefinition, + }) + + return { revocationRegistryDefinition, revocationRegistryDefinitionTxnTime: response.result.txnTime } + } catch (error) { + this.logger.error( + `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, + { + error, + revocationRegistryDefinitionId: revocationRegistryDefinitionId, + pool: pool.id, + } + ) + throw error + } + } + + //Retrieves the accumulated state of a revocation registry by id given a revocation interval from & to (used primarily for proof creation) + public async getRevocationRegistryDelta( + revocationRegistryDefinitionId: string, + to: number = new Date().getTime(), + from = 0 + ): Promise { + //TODO - implement a cache + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry delta with revocation registry definition id: '${revocationRegistryDefinitionId}'`, + { + to, + from, + } + ) + + try { + const request = await this.indy.buildGetRevocRegDeltaRequest(null, revocationRegistryDefinitionId, from, to) + + this.logger.trace( + `Submitting get revocation registry delta request for revocation registry '${revocationRegistryDefinitionId}' to ledger` + ) + + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got revocation registry delta unparsed-response '${revocationRegistryDefinitionId}' from ledger`, + { + response, + } + ) + + const [, revocationRegistryDelta, deltaTimestamp] = await this.indy.parseGetRevocRegDeltaResponse(response) + + this.logger.debug(`Got revocation registry delta '${revocationRegistryDefinitionId}' from ledger`, { + revocationRegistryDelta, + deltaTimestamp, + to, + from, + }) + + return { revocationRegistryDelta, deltaTimestamp } + } catch (error) { + this.logger.error( + `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, + { + error, + revocationRegistryId: revocationRegistryDefinitionId, + pool: pool.id, + } + ) + throw error + } + } + + //Retrieves the accumulated state of a revocation registry by id given a timestamp (used primarily for verification) + public async getRevocationRegistry( + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + //TODO - implement a cache + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry accumulated state with revocation registry definition id: '${revocationRegistryDefinitionId}'`, + { + timestamp, + } + ) + + try { + const request = await this.indy.buildGetRevocRegRequest(null, revocationRegistryDefinitionId, timestamp) + + this.logger.trace( + `Submitting get revocation registry request for revocation registry '${revocationRegistryDefinitionId}' to ledger` + ) + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got un-parsed revocation registry '${revocationRegistryDefinitionId}' from ledger '${pool.id}'`, + { + response, + } + ) + + const [, revocationRegistry, ledgerTimestamp] = await this.indy.parseGetRevocRegResponse(response) + this.logger.debug(`Got revocation registry '${revocationRegistryDefinitionId}' from ledger`, { + ledgerTimestamp, + revocationRegistry, + }) + + return { revocationRegistry, ledgerTimestamp } + } catch (error) { + this.logger.error(`Error retrieving revocation registry '${revocationRegistryDefinitionId}' from ledger`, { + error, + revocationRegistryId: revocationRegistryDefinitionId, + pool: pool.id, + }) + throw error + } + } + private async submitWriteRequest( pool: IndyPool, request: LedgerRequest, @@ -369,6 +527,21 @@ export interface CredentialDefinitionTemplate { supportRevocation: boolean } +export interface ParseRevocationRegistryDefitinionTemplate { + revocationRegistryDefinition: Indy.RevocRegDef + revocationRegistryDefinitionTxnTime: number +} + +export interface ParseRevocationRegistryDeltaTemplate { + revocationRegistryDelta: Indy.RevocRegDelta + deltaTimestamp: number +} + +export interface ParseRevocationRegistryTemplate { + revocationRegistry: Indy.RevocReg + ledgerTimestamp: number +} + export interface IndyEndpointAttrib { endpoint?: string types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index f7ffdb56b6..b4a934c9f1 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -354,7 +354,9 @@ export class ProofsModule { ) } - return this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, presentationPreview) + return this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { + presentationProposal: presentationPreview, + }) } /** diff --git a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts index 4143fcc0dd..c8c1385a39 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts @@ -9,6 +9,7 @@ import { InboundMessageContext } from '../../../agent/models/InboundMessageConte import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' import { ConnectionService, ConnectionState } from '../../connections' import { IndyHolderService } from '../../indy/services/IndyHolderService' +import { IndyRevocationService } from '../../indy/services/IndyRevocationService' import { IndyLedgerService } from '../../ledger/services' import { ProofEventTypes } from '../ProofEvents' import { ProofState } from '../ProofState' @@ -29,6 +30,7 @@ jest.mock('../../../modules/ledger/services/IndyLedgerService') jest.mock('../../indy/services/IndyHolderService') jest.mock('../../indy/services/IndyIssuerService') jest.mock('../../indy/services/IndyVerifierService') +jest.mock('../../indy/services/IndyRevocationService') jest.mock('../../connections/services/ConnectionService') // Mock typed object @@ -36,6 +38,7 @@ const ProofRepositoryMock = ProofRepository as jest.Mock const IndyLedgerServiceMock = IndyLedgerService as jest.Mock const IndyHolderServiceMock = IndyHolderService as jest.Mock const IndyVerifierServiceMock = IndyVerifierService as jest.Mock +const IndyRevocationServiceMock = IndyRevocationService as jest.Mock const connectionServiceMock = ConnectionService as jest.Mock const connection = getMockConnection({ @@ -93,6 +96,7 @@ describe('ProofService', () => { let wallet: Wallet let indyVerifierService: IndyVerifierService let indyHolderService: IndyHolderService + let indyRevocationService: IndyRevocationService let eventEmitter: EventEmitter let credentialRepository: CredentialRepository let connectionService: ConnectionService @@ -102,6 +106,7 @@ describe('ProofService', () => { proofRepository = new ProofRepositoryMock() indyVerifierService = new IndyVerifierServiceMock() indyHolderService = new IndyHolderServiceMock() + indyRevocationService = new IndyRevocationServiceMock() ledgerService = new IndyLedgerServiceMock() eventEmitter = new EventEmitter(agentConfig) connectionService = new connectionServiceMock() @@ -113,6 +118,7 @@ describe('ProofService', () => { agentConfig, indyHolderService, indyVerifierService, + indyRevocationService, connectionService, eventEmitter, credentialRepository diff --git a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts index fdeb944f36..bbedf910a6 100644 --- a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts @@ -41,19 +41,20 @@ export class RequestPresentationHandler implements Handler { messageContext: HandlerInboundMessage ) { const indyProofRequest = record.requestMessage?.indyProofRequest + const presentationProposal = record.proposalMessage?.presentationProposal this.agentConfig.logger.info( `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` ) if (!indyProofRequest) { + this.agentConfig.logger.error('Proof request is undefined.') return } - const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest( - indyProofRequest, - record.proposalMessage?.presentationProposal - ) + const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { + presentationProposal, + }) const requestedCredentials = this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) diff --git a/packages/core/src/modules/proofs/models/RequestedAttribute.ts b/packages/core/src/modules/proofs/models/RequestedAttribute.ts index 929759ef10..4998a8b097 100644 --- a/packages/core/src/modules/proofs/models/RequestedAttribute.ts +++ b/packages/core/src/modules/proofs/models/RequestedAttribute.ts @@ -13,6 +13,7 @@ export class RequestedAttribute { this.timestamp = options.timestamp this.revealed = options.revealed this.credentialInfo = options.credentialInfo + this.revoked = options.revoked } } @@ -29,5 +30,8 @@ export class RequestedAttribute { public revealed!: boolean @Exclude({ toPlainOnly: true }) - public credentialInfo!: IndyCredentialInfo + public credentialInfo?: IndyCredentialInfo + + @Exclude({ toPlainOnly: true }) + public revoked?: boolean } diff --git a/packages/core/src/modules/proofs/models/RequestedPredicate.ts b/packages/core/src/modules/proofs/models/RequestedPredicate.ts index f4d08657b1..5e7d4dc5f9 100644 --- a/packages/core/src/modules/proofs/models/RequestedPredicate.ts +++ b/packages/core/src/modules/proofs/models/RequestedPredicate.ts @@ -12,6 +12,7 @@ export class RequestedPredicate { this.credentialId = options.credentialId this.timestamp = options.timestamp this.credentialInfo = options.credentialInfo + this.revoked = options.revoked } } @@ -25,5 +26,8 @@ export class RequestedPredicate { public timestamp?: number @Exclude({ toPlainOnly: true }) - public credentialInfo!: IndyCredentialInfo + public credentialInfo?: IndyCredentialInfo + + @Exclude({ toPlainOnly: true }) + public revoked?: boolean } diff --git a/packages/core/src/modules/proofs/services/ProofService.ts b/packages/core/src/modules/proofs/services/ProofService.ts index 95477939e9..aec5bacbb8 100644 --- a/packages/core/src/modules/proofs/services/ProofService.ts +++ b/packages/core/src/modules/proofs/services/ProofService.ts @@ -22,8 +22,8 @@ import { uuid } from '../../../utils/uuid' import { Wallet } from '../../../wallet/Wallet' import { AckStatus } from '../../common' import { ConnectionService } from '../../connections' -import { CredentialUtils, Credential, CredentialRepository } from '../../credentials' -import { IndyHolderService, IndyVerifierService } from '../../indy' +import { CredentialUtils, Credential, CredentialRepository, IndyCredentialInfo } from '../../credentials' +import { IndyHolderService, IndyVerifierService, IndyRevocationService } from '../../indy' import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' import { ProofEventTypes } from '../ProofEvents' import { ProofState } from '../ProofState' @@ -64,6 +64,7 @@ export class ProofService { private logger: Logger private indyHolderService: IndyHolderService private indyVerifierService: IndyVerifierService + private indyRevocationService: IndyRevocationService private connectionService: ConnectionService private eventEmitter: EventEmitter @@ -74,6 +75,7 @@ export class ProofService { agentConfig: AgentConfig, indyHolderService: IndyHolderService, indyVerifierService: IndyVerifierService, + indyRevocationService: IndyRevocationService, connectionService: ConnectionService, eventEmitter: EventEmitter, credentialRepository: CredentialRepository @@ -85,6 +87,7 @@ export class ProofService { this.logger = agentConfig.logger this.indyHolderService = indyHolderService this.indyVerifierService = indyVerifierService + this.indyRevocationService = indyRevocationService this.connectionService = connectionService this.eventEmitter = eventEmitter } @@ -699,6 +702,12 @@ export class ProofService { // List the requested attributes requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) + //Get credentialInfo + if (!requestedAttribute.credentialInfo) { + const indyCredentialInfo = await this.indyHolderService.getCredential(requestedAttribute.credentialId) + requestedAttribute.credentialInfo = JsonTransformer.fromJSON(indyCredentialInfo, IndyCredentialInfo) + } + // Find the attributes that have a hashlink as a value for (const attribute of Object.values(requestedAttribute.credentialInfo.attributes)) { if (attribute.toLowerCase().startsWith('hl:')) { @@ -746,7 +755,9 @@ export class ProofService { */ public async getRequestedCredentialsForProofRequest( proofRequest: ProofRequest, - presentationProposal?: PresentationPreview + config: { + presentationProposal?: PresentationPreview + } = {} ): Promise { const retrievedCredentials = new RetrievedCredentials({}) @@ -756,7 +767,7 @@ export class ProofService { // If we have exactly one credential, or no proposal to pick preferences // on the credentials to use, we will use the first one - if (credentials.length === 1 || !presentationProposal) { + if (credentials.length === 1 || !config.presentationProposal) { credentialMatch = credentials } // If we have a proposal we will use that to determine the credentials to use @@ -769,7 +780,7 @@ export class ProofService { // Check if credentials matches all parameters from proposal return names.every((name) => - presentationProposal.attributes.find( + config.presentationProposal?.attributes.find( (a) => a.name === name && a.credentialDefinitionId === credentialDefinitionId && @@ -779,24 +790,86 @@ export class ProofService { }) } - retrievedCredentials.requestedAttributes[referent] = credentialMatch.map((credential: Credential) => { - return new RequestedAttribute({ - credentialId: credential.credentialInfo.referent, - revealed: true, - credentialInfo: credential.credentialInfo, + retrievedCredentials.requestedAttributes[referent] = await Promise.all( + credentialMatch.map(async (credential: Credential) => { + const requestNonRevoked = requestedAttribute.nonRevoked ?? proofRequest.nonRevoked + const credentialRevocationId = credential.credentialInfo.credentialRevocationId + const revocationRegistryId = credential.credentialInfo.revocationRegistryId + let revoked: boolean | undefined + let deltaTimestamp: number | undefined + + // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display + if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation for referent '${referent}', getting revocation status for credential`, + { + requestNonRevoked, + credentialRevocationId, + revocationRegistryId, + } + ) + + // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + const status = await this.indyRevocationService.getRevocationStatus( + credentialRevocationId, + revocationRegistryId, + requestNonRevoked + ) + revoked = status.revoked + deltaTimestamp = status.deltaTimestamp + } + + return new RequestedAttribute({ + credentialId: credential.credentialInfo.referent, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - }) + ) } - for (const [referent] of proofRequest.requestedPredicates.entries()) { + for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) - retrievedCredentials.requestedPredicates[referent] = credentials.map((credential) => { - return new RequestedPredicate({ - credentialId: credential.credentialInfo.referent, - credentialInfo: credential.credentialInfo, + retrievedCredentials.requestedPredicates[referent] = await Promise.all( + credentials.map(async (credential) => { + const requestNonRevoked = requestedPredicate.nonRevoked ?? proofRequest.nonRevoked + const credentialRevocationId = credential.credentialInfo.credentialRevocationId + const revocationRegistryId = credential.credentialInfo.revocationRegistryId + let revoked: boolean | undefined + let deltaTimestamp: number | undefined + + // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display + if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation for referent '${referent}', getting revocation status for credential`, + { + requestNonRevoked, + credentialRevocationId, + revocationRegistryId, + } + ) + + // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + const status = await this.indyRevocationService.getRevocationStatus( + credentialRevocationId, + revocationRegistryId, + requestNonRevoked + ) + revoked = status.revoked + deltaTimestamp = status.deltaTimestamp + } + + return new RequestedPredicate({ + credentialId: credential.credentialInfo.referent, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - }) + ) } return retrievedCredentials @@ -947,24 +1020,30 @@ export class ProofService { proofRequest: ProofRequest, requestedCredentials: RequestedCredentials ): Promise { - const credentialObjects = [ - ...Object.values(requestedCredentials.requestedAttributes), - ...Object.values(requestedCredentials.requestedPredicates), - ].map((c) => c.credentialInfo) + const credentialObjects = await Promise.all( + [ + ...Object.values(requestedCredentials.requestedAttributes), + ...Object.values(requestedCredentials.requestedPredicates), + ].map(async (c) => { + if (c.credentialInfo) { + return c.credentialInfo + } + const credentialInfo = await this.indyHolderService.getCredential(c.credentialId) + return JsonTransformer.fromJSON(credentialInfo, IndyCredentialInfo) + }) + ) const schemas = await this.getSchemas(new Set(credentialObjects.map((c) => c.schemaId))) const credentialDefinitions = await this.getCredentialDefinitions( new Set(credentialObjects.map((c) => c.credentialDefinitionId)) ) - const proof = await this.indyHolderService.createProof({ + return this.indyHolderService.createProof({ proofRequest: proofRequest.toJSON(), - requestedCredentials: requestedCredentials.toJSON(), + requestedCredentials: requestedCredentials, schemas, credentialDefinitions, }) - - return proof } private async getCredentialsForProofRequest( diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index 7fdeb53c52..6673bc333c 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -4,4 +4,5 @@ export interface FileSystem { exists(path: string): Promise write(path: string, data: string): Promise read(path: string): Promise + downloadToFile(url: string, path: string): Promise } diff --git a/packages/core/src/utils/did.ts b/packages/core/src/utils/did.ts index 566ca4791b..b56df0c6c3 100644 --- a/packages/core/src/utils/did.ts +++ b/packages/core/src/utils/did.ts @@ -68,6 +68,15 @@ export function getFullVerkey(did: string, verkey: string) { ) } +/** + * Extract did from schema id + */ +export function didFromSchemaId(schemaId: string) { + const [did] = schemaId.split(':') + + return did +} + /** * Extract did from credential definition id */ @@ -78,10 +87,10 @@ export function didFromCredentialDefinitionId(credentialDefinitionId: string) { } /** - * Extract did from schema id + * Extract did from revocation registry definition id */ -export function didFromSchemaId(schemaId: string) { - const [did] = schemaId.split(':') +export function didFromRevocationRegistryDefinitionId(revocationRegistryId: string) { + const [did] = revocationRegistryId.split(':') return did } diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index daafb1f28c..f739c40814 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -1,6 +1,8 @@ import type { FileSystem } from '@aries-framework/core' -import { promises } from 'fs' +import fs, { promises } from 'fs' +import http from 'http' +import https from 'https' import { tmpdir } from 'os' import { dirname } from 'path' @@ -37,4 +39,34 @@ export class NodeFileSystem implements FileSystem { public async read(path: string): Promise { return readFile(path, { encoding: 'utf-8' }) } + + public async downloadToFile(url: string, path: string) { + const httpMethod = url.startsWith('https') ? https : http + + // Make sure parent directories exist + await promises.mkdir(dirname(path), { recursive: true }) + + const file = fs.createWriteStream(path) + + return new Promise((resolve, reject) => { + httpMethod + .get(url, (response) => { + // check if response is success + if (response.statusCode !== 200) { + reject(`Unable to download file from url: ${url}. Response status was ${response.statusCode}`) + } + + response.pipe(file) + file.on('finish', () => { + file.close() + resolve() + }) + }) + .on('error', async (error) => { + // Handle errors + await fs.promises.unlink(path) // Delete the file async. (But we don't check the result) + reject(`Unable to download file from url: ${url}. ${error.message}`) + }) + }) + } } diff --git a/packages/react-native/package.json b/packages/react-native/package.json index f864a85a3f..2139e45894 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -29,7 +29,7 @@ "events": "^3.3.0" }, "devDependencies": { - "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.8", + "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.12", "@types/react-native": "^0.64.10", "indy-sdk-react-native": "^0.1.16", "react": "17.0.1", diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index eacba6f5b0..331fa11a54 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -31,4 +31,16 @@ export class ReactNativeFileSystem implements FileSystem { public async read(path: string): Promise { return RNFS.readFile(path, 'utf8') } + + public async downloadToFile(url: string, path: string) { + // Make sure parent directories exist + await RNFS.mkdir(getDirFromFilePath(path)) + + const { promise } = RNFS.downloadFile({ + fromUrl: url, + toFile: path, + }) + + await promise + } } diff --git a/yarn.lock b/yarn.lock index ff41e55c94..8c017a7033 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2237,10 +2237,10 @@ dependencies: "@types/node" "*" -"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.8", "@types/indy-sdk@^1.16.8": - version "1.16.9" - resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.9.tgz#647a77ead1e93c77b2b0ef5f2c399ba2ea461b89" - integrity sha512-X8fdwcGaXfCxayBOb4NUny37JDd9Q3ZDKnm7WBhPRcdOXw3kmsy+WX52751nJQRBcltu883rbqYAIzcZE83XRA== +"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.10", "@types/indy-sdk@^1.16.10": + version "1.16.10" + resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.10.tgz#cb13c0c639ce63758eecf534dc01111dc1c42633" + integrity sha512-zcSBMiDyareFHgDF/RpeWvboFTBTnHLi/+SK8pb7UL+o9WIHW6W8ZuLkinOiu58MvPlKceSjEx8dAl/+yoW2JA== dependencies: buffer "^6.0.0"