diff --git a/src/components/jsonld/JsonLdCredentialViewer.tsx b/src/components/jsonld/JsonLdCredentialViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a12dba3324252acf8e3d460aae1c50f72d61c518 --- /dev/null +++ b/src/components/jsonld/JsonLdCredentialViewer.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import JsonLdRawViewer from "./JsonLdRawViewer"; +import VereignLegalParticipant from "./VereignLegalParticipant"; +import VereignPrivatePerson from "./VereignPrivatePerson"; + +type JsonLdCredentialViewerProps = { + jsonld: any; +}; + +const JsonLdCredentialViewer: React.FC<JsonLdCredentialViewerProps> = ({ + jsonld +}) => { + const subject = jsonld?.credentialSubject || {}; + const subjectType = subject.type; + switch (subjectType) { + case "vereign:PrivatePerson": + return <VereignPrivatePerson jsonld={jsonld} />; + case "vereign:LegalParticipant": + return <VereignLegalParticipant jsonld={jsonld} />; + default: + return <JsonLdRawViewer jsonld={jsonld} />; + } +}; + +export default JsonLdCredentialViewer; diff --git a/src/components/jsonld/JsonLdRawViewer.tsx b/src/components/jsonld/JsonLdRawViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2504b61bfcb88028a7d4953ad7da0a23d52cbd4d --- /dev/null +++ b/src/components/jsonld/JsonLdRawViewer.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import {ColorPallet, TextTheme} from 'src/theme/theme'; + +type LegalParticipantProps = { + jsonld: any; +}; + +const JsonLdRawViewer: React.FC<LegalParticipantProps> = ({ + jsonld, +}) => { + return ( + <View> + <Text style={styles.header}>Raw JSON-LD:</Text> + <View style={styles.json}> + <Text style={styles.jsonValue}> + {JSON.stringify(jsonld, null, 2)} + </Text> + </View> + </View> + ); +}; + +export default JsonLdRawViewer; + +const styles = StyleSheet.create({ + header: { + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + fontWeight: 'bold', + marginBottom: 8, + }, + json: { + paddingVertical: 16, + }, + jsonValue: { + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + }, +}); diff --git a/src/components/jsonld/VereignLegalParticipant.tsx b/src/components/jsonld/VereignLegalParticipant.tsx new file mode 100644 index 0000000000000000000000000000000000000000..76a6922fa78b5a9d3064835ce84e2f36648c60e6 --- /dev/null +++ b/src/components/jsonld/VereignLegalParticipant.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import {ColorPallet, TextTheme} from 'src/theme/theme'; + +type LegalParticipantProps = { + jsonld: any; +}; + +const VereignLegalParticipant: React.FC<LegalParticipantProps> = ({ + jsonld, +}) => { + const subject = jsonld?.credentialSubject || {}; + + const companyName = subject["vereign:companyName"]; + const taxID = subject["vereign:taxID"]; + const gleiCode = subject["vereign:gleiCode"]; + + const address = subject["vereign:address"] || {}; + const street = address["vereign:street"]; + const building = address["vereign:building"]; + const city = address["vereign:city"]; + const country = address["vereign:country"]; + + return ( + <View> + <Text style={styles.header}>JSON-LD</Text> + <View style={styles.prop}> + <Text style={styles.propLabel}>CompanyName</Text> + <Text style={styles.propValue}>{companyName}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>TaxID</Text> + <Text style={styles.propValue}>{taxID}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>GleiCode</Text> + <Text style={styles.propValue}>{gleiCode}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Street</Text> + <Text style={styles.propValue}>{street}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Building</Text> + <Text style={styles.propValue}>{building}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>City</Text> + <Text style={styles.propValue}>{city}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Country</Text> + <Text style={styles.propValue}>{country}</Text> + </View> + </View> + ); +}; + +export default VereignLegalParticipant; + +const styles = StyleSheet.create({ + header: { + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + fontWeight: 'bold', + marginBottom: 8, + }, + prop: { + flexDirection: 'row', + paddingVertical: 4, + }, + propLabel: { + width: '40%', + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + fontWeight: 'bold', + }, + propValue: { + width: '60%', + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + textAlign: 'right', + }, +}); diff --git a/src/components/jsonld/VereignPrivatePerson.tsx b/src/components/jsonld/VereignPrivatePerson.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6c9f2ed93820d5c0c41327046e4adcc4d001560a --- /dev/null +++ b/src/components/jsonld/VereignPrivatePerson.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import {ColorPallet, TextTheme} from 'src/theme/theme'; + +type PrivatePersonProps = { + jsonld: any; +}; + +const VereignPrivatePerson: React.FC<PrivatePersonProps> = ({ + jsonld, +}) => { + const subject = jsonld.credentialSubject || {}; + + const name = subject["vereign:name"]; + const dateOfBirth = subject["vereign:dateOfBirth"]; + + const address = subject["vereign:address"] || {}; + const street = address["vereign:street"]; + const building = address["vereign:building"]; + const city = address["vereign:city"]; + const country = address["vereign:country"]; + + return ( + <View> + <Text style={styles.header}>JSON-LD</Text> + <View style={styles.prop}> + <Text style={styles.propLabel}>Name</Text> + <Text style={styles.propValue}>{name}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>DateOfBirth</Text> + <Text style={styles.propValue}>{dateOfBirth}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Street</Text> + <Text style={styles.propValue}>{street}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Building</Text> + <Text style={styles.propValue}>{building}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>City</Text> + <Text style={styles.propValue}>{city}</Text> + </View> + <View style={styles.prop}> + <Text style={styles.propLabel}>Country</Text> + <Text style={styles.propValue}>{country}</Text> + </View> + </View> + ); +}; + +export default VereignPrivatePerson; + +const styles = StyleSheet.create({ + header: { + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + fontWeight: 'bold', + marginBottom: 8, + }, + prop: { + flexDirection: 'row', + paddingVertical: 4, + }, + propLabel: { + width: '40%', + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + fontWeight: 'bold', + }, + propValue: { + width: '60%', + ...TextTheme.normal, + color: ColorPallet.baseColors.black, + textAlign: 'right', + }, +}); diff --git a/src/components/notifications/NotificationListItem.tsx b/src/components/notifications/NotificationListItem.tsx index 72e9f53d372054e0dc69f4d2d72465b9e8a9e096..5c4b1f20cad284b6d29d7ed1a225f34995aaa307 100644 --- a/src/components/notifications/NotificationListItem.tsx +++ b/src/components/notifications/NotificationListItem.tsx @@ -23,7 +23,7 @@ const NotificationListItem: React.FC<NotificationListItemProps> = ({ useEffect(() => { (async () => { - console.log(notification); + console.log(JSON.stringify(notification, null, 2)); if (notification.proofRequest) { setTitle(t<string>('ProofRequest.ProofRequest')); setBody(notification.connection?.theirLabel ?? 'Connectionless proof request'); diff --git a/src/credo/JsonLdCredentialFormatService.ts b/src/credo/JsonLdCredentialFormatService.ts new file mode 100644 index 0000000000000000000000000000000000000000..c50c2cea59f559d9ba002788b9106659defee624 --- /dev/null +++ b/src/credo/JsonLdCredentialFormatService.ts @@ -0,0 +1,587 @@ +/** + * This is a copy of https://code.vereign.com/gaiax/ocm/ocm-engine/-/blob/main/libs/askar/src/credo/JsonLdCredentialFormatService.ts?ref_type=heads + * TODO do we need to move this code bellow and code by the link at OCM-Engine to separate project? (this files are the same) + */ + +import { + JsonLdCredentialFormat, + JsonCredential, + JsonLdFormatDataCredentialDetail, + JsonLdFormatDataVerifiableCredential, + AgentContext, + CredentialFormatService, + Attachment, + AttachmentData, + AriesFrameworkError, + JsonEncoder, + JsonTransformer, + findVerificationMethodByKeyType, + DidResolverService, + ClaimFormat, + W3cCredential, + W3cCredentialService, + W3cJsonLdVerifiableCredential, + CredentialFormatSpec, + JsonLdCredentialDetail, +} from "@aries-framework/core"; +import type { + CredentialFormatAcceptOfferOptions, + CredentialFormatAcceptProposalOptions, + CredentialFormatAcceptRequestOptions, + CredentialFormatAutoRespondOfferOptions, + CredentialFormatAutoRespondProposalOptions, + CredentialFormatAutoRespondRequestOptions, + CredentialFormatCreateOfferOptions, + CredentialFormatCreateOfferReturn, + CredentialFormatCreateProposalOptions, + CredentialFormatCreateProposalReturn, + CredentialFormatCreateRequestOptions, + CredentialFormatCreateReturn, + CredentialFormatProcessCredentialOptions, + CredentialFormatProcessOptions, + CredentialFormatAutoRespondCredentialOptions, +} from "@aries-framework/core"; +import { areObjectsEqual } from "@aries-framework/core/build/utils"; +import { W3cJsonLdCredentialService } from "@aries-framework/core/build/modules/vc/data-integrity/W3cJsonLdCredentialService"; + +const JSONLD_VC_DETAIL = "aries/ld-proof-vc-detail@v1.0"; +const JSONLD_VC = "aries/ld-proof-vc@v1.0"; + +export class JsonLdCredentialFormatService + implements CredentialFormatService<JsonLdCredentialFormat> +{ + public readonly formatKey = "jsonld" as const; + public readonly credentialRecordType = "w3c" as const; + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @returns object containing associated attachment, formats and filtersAttach elements + * + */ + public async createProposal( + agentContext: AgentContext, + { + credentialFormats, + }: CredentialFormatCreateProposalOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateProposalReturn> { + const format = new CredentialFormatSpec({ + format: JSONLD_VC_DETAIL, + }); + + const jsonLdFormat = credentialFormats.jsonld; + if (!jsonLdFormat) { + throw new AriesFrameworkError("Missing jsonld payload in createProposal"); + } + + // this does the validation + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail); + + // jsonLdFormat is now of type JsonLdFormatDataCredentialDetail + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId); + return { format, attachment }; + } + + /** + * Method called on reception of a propose credential message + * @param options the options needed to accept the proposal + */ + public async processProposal( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions, + ): Promise<void> { + const credProposalJson = + attachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + if (!credProposalJson) { + throw new AriesFrameworkError( + "Missing jsonld credential proposal data payload", + ); + } + + // validation is done in here + JsonTransformer.fromJSON(credProposalJson, JsonLdCredentialDetail); + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + proposalAttachment, + }: CredentialFormatAcceptProposalOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateOfferReturn> { + // if the offer has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }); + + const credentialProposal = + proposalAttachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + JsonTransformer.fromJSON(credentialProposal, JsonLdCredentialDetail); + + const offerData = credentialProposal; + + const attachment = this.getFormatData(offerData, format.attachmentId); + + return { format, attachment }; + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the credential offer + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer( + agentContext: AgentContext, + { + credentialFormats, + attachmentId, + }: CredentialFormatCreateOfferOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateOfferReturn> { + // if the offer has an attachment Id use that, otherwise the generated id of the formats object + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }); + + const jsonLdFormat = credentialFormats?.jsonld; + if (!jsonLdFormat) { + throw new AriesFrameworkError("Missing jsonld payload in createOffer"); + } + + // validate + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail); + + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId); + + return { format, attachment }; + } + + public async processOffer( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions, + ) { + const credentialOfferJson = + attachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + if (!credentialOfferJson) { + throw new AriesFrameworkError( + "Missing jsonld credential offer data payload", + ); + } + + JsonTransformer.fromJSON(credentialOfferJson, JsonLdCredentialDetail); + } + + public async acceptOffer( + agentContext: AgentContext, + { + attachmentId, + offerAttachment, + }: CredentialFormatAcceptOfferOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateReturn> { + const credentialOffer = + offerAttachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + // validate + JsonTransformer.fromJSON(credentialOffer, JsonLdCredentialDetail); + + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC_DETAIL, + }); + + const attachment = this.getFormatData(credentialOffer, format.attachmentId); + return { format, attachment }; + } + + /** + * Create a credential attachment format for a credential request. + * + * @param options The object containing all the options for the credential request is derived + * @returns object containing associated attachment, formats and requestAttach elements + * + */ + public async createRequest( + agentContext: AgentContext, + { + credentialFormats, + }: CredentialFormatCreateRequestOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateReturn> { + const jsonLdFormat = credentialFormats?.jsonld; + + const format = new CredentialFormatSpec({ + format: JSONLD_VC_DETAIL, + }); + + if (!jsonLdFormat) { + throw new AriesFrameworkError("Missing jsonld payload in createRequest"); + } + + // this does the validation + JsonTransformer.fromJSON(jsonLdFormat.credential, JsonLdCredentialDetail); + + const attachment = this.getFormatData(jsonLdFormat, format.attachmentId); + + return { format, attachment }; + } + + public async processRequest( + agentContext: AgentContext, + { attachment }: CredentialFormatProcessOptions, + ): Promise<void> { + const requestJson = + attachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + if (!requestJson) { + throw new AriesFrameworkError( + "Missing jsonld credential request data payload", + ); + } + + // validate + JsonTransformer.fromJSON(requestJson, JsonLdCredentialDetail); + } + + public async acceptRequest( + agentContext: AgentContext, + { + credentialFormats, + attachmentId, + requestAttachment, + }: CredentialFormatAcceptRequestOptions<JsonLdCredentialFormat>, + ): Promise<CredentialFormatCreateReturn> { + const w3cJsonLdCredentialService = agentContext.dependencyManager.resolve( + W3cJsonLdCredentialService, + ); + + // sign credential here. credential to be signed is received as the request attachment + // (attachment in the request message from holder to issuer) + const credentialRequest = + requestAttachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + const verificationMethod = + credentialFormats?.jsonld?.verificationMethod ?? + (await this.deriveVerificationMethod( + agentContext, + credentialRequest.credential, + credentialRequest, + )); + + if (!verificationMethod) { + throw new AriesFrameworkError( + "Missing verification method in credential data", + ); + } + const format = new CredentialFormatSpec({ + attachmentId, + format: JSONLD_VC, + }); + + const options = credentialRequest.options; + + // Get a list of fields found in the options that are not supported at the moment + const unsupportedFields = [ + "challenge", + "domain", + "credentialStatus", + "created", + ] as const; + const foundFields = unsupportedFields.filter( + (field) => options[field] !== undefined, + ); + + if (foundFields.length > 0) { + throw new AriesFrameworkError( + `Some fields are not currently supported in credential options: ${foundFields.join( + ", ", + )}`, + ); + } + + const credential = JsonTransformer.fromJSON( + credentialRequest.credential, + W3cCredential, + ); + + const verifiableCredential = + await w3cJsonLdCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + proofType: credentialRequest.options.proofType, + verificationMethod: verificationMethod, + }); + + const attachment = this.getFormatData( + JsonTransformer.toJSON(verifiableCredential), + format.attachmentId, + ); + return { format, attachment }; + } + + /** + * Derive a verification method using the issuer from the given verifiable credential + * @param credentialAsJson the verifiable credential we want to sign + * @return the verification method derived from this credential and its associated issuer did, keys etc. + */ + private async deriveVerificationMethod( + agentContext: AgentContext, + credentialAsJson: JsonCredential, + credentialRequest: JsonLdFormatDataCredentialDetail, + ): Promise<string> { + const didResolver = + agentContext.dependencyManager.resolve(DidResolverService); + const w3cJsonLdCredentialService = agentContext.dependencyManager.resolve( + W3cJsonLdCredentialService, + ); + + const credential = JsonTransformer.fromJSON( + credentialAsJson, + W3cCredential, + ); + + // extract issuer from vc (can be string or Issuer) + let issuerDid = credential.issuer; + + if (typeof issuerDid !== "string") { + issuerDid = issuerDid.id; + } + // this will throw an error if the issuer did is invalid + const issuerDidDocument = await didResolver.resolveDidDocument( + agentContext, + issuerDid, + ); + + // find first key which matches proof type + const proofType = credentialRequest.options.proofType; + + // actually gets the key type(s) + const keyType = + w3cJsonLdCredentialService.getVerificationMethodTypesByProofType( + proofType, + ); + + if (!keyType || keyType.length === 0) { + throw new AriesFrameworkError( + `No Key Type found for proofType ${proofType}`, + ); + } + + const verificationMethod = await findVerificationMethodByKeyType( + keyType[0], + issuerDidDocument, + ); + if (!verificationMethod) { + throw new AriesFrameworkError( + `Missing verification method for key type ${keyType}`, + ); + } + + return verificationMethod.id; + } + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + agentContext: AgentContext, + { + credentialRecord, + attachment, + requestAttachment, + }: CredentialFormatProcessCredentialOptions, + ): Promise<void> { + const w3cCredentialService = + agentContext.dependencyManager.resolve(W3cCredentialService); + + const credentialAsJson = attachment.getDataAsJson(); + const credential = JsonTransformer.fromJSON( + credentialAsJson, + W3cJsonLdVerifiableCredential, + ); + const requestAsJson = + requestAttachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + // Verify the credential request matches the credential + this.verifyReceivedCredentialMatchesRequest(credential, requestAsJson); + + // verify signatures of the credential + const result = await w3cCredentialService.verifyCredential(agentContext, { + credential, + }); + if (result && !result.isValid) { + throw new AriesFrameworkError( + `Failed to validate credential, error = ${result.error}`, + ); + } + + const verifiableCredential = await w3cCredentialService.storeCredential( + agentContext, + { + credential, + }, + ); + + credentialRecord.credentials.push({ + credentialRecordType: this.credentialRecordType, + credentialRecordId: verifiableCredential.id, + }); + } + + private verifyReceivedCredentialMatchesRequest( + credential: W3cJsonLdVerifiableCredential, + request: JsonLdFormatDataCredentialDetail, + ): void { + const jsonCredential = JsonTransformer.toJSON(credential); + delete jsonCredential["proof"]; + + const credentialProof = Array.isArray(credential.proof) + ? credential.proof[credential.proof.length - 1] + : credential.proof; + + if ( + request.options.created && + credentialProof.created !== request.options.created + ) { + throw new AriesFrameworkError( + "Received credential proof created does not match created from credential request", + ); + } + + if (credentialProof.domain !== request.options.domain) { + throw new AriesFrameworkError( + "Received credential proof domain does not match domain from credential request", + ); + } + + if (credentialProof.challenge !== request.options.challenge) { + throw new AriesFrameworkError( + "Received credential proof challenge does not match challenge from credential request", + ); + } + + if (credentialProof.type !== request.options.proofType) { + throw new AriesFrameworkError( + "Received credential proof type does not match proof type from credential request", + ); + } + + if (credentialProof.proofPurpose !== request.options.proofPurpose) { + throw new AriesFrameworkError( + "Received credential proof purpose does not match proof purpose from credential request", + ); + } + + // Check whether the received credential (minus the proof) matches the credential request + const requestJsonCredential = JsonTransformer.toJSON(request.credential); + delete requestJsonCredential["proof"]; + if (!areObjectsEqual(jsonCredential, requestJsonCredential)) { + throw new AriesFrameworkError( + "Received credential does not match credential request", + ); + } + + // TODO: add check for the credentialStatus once this is supported in Credo + } + + public supportsFormat(format: string): boolean { + const supportedFormats = [JSONLD_VC_DETAIL, JSONLD_VC]; + + return supportedFormats.includes(format); + } + + public async deleteCredentialById(): Promise<void> { + throw new Error("Not implemented."); + } + + public areCredentialsEqual = ( + message1: Attachment, + message2: Attachment, + ): boolean => { + const obj1 = message1.getDataAsJson(); + const obj2 = message2.getDataAsJson(); + + return areObjectsEqual(obj1, obj2); + }; + + public async shouldAutoRespondToProposal( + agentContext: AgentContext, + { + offerAttachment, + proposalAttachment, + }: CredentialFormatAutoRespondProposalOptions, + ) { + return this.areCredentialsEqual(proposalAttachment, offerAttachment); + } + + public async shouldAutoRespondToOffer( + agentContext: AgentContext, + { + offerAttachment, + proposalAttachment, + }: CredentialFormatAutoRespondOfferOptions, + ) { + return this.areCredentialsEqual(proposalAttachment, offerAttachment); + } + + public async shouldAutoRespondToRequest( + agentContext: AgentContext, + { + offerAttachment, + requestAttachment, + }: CredentialFormatAutoRespondRequestOptions, + ) { + return this.areCredentialsEqual(offerAttachment, requestAttachment); + } + + public async shouldAutoRespondToCredential( + agentContext: AgentContext, + { + requestAttachment, + credentialAttachment, + }: CredentialFormatAutoRespondCredentialOptions, + ) { + const credentialJson = + credentialAttachment.getDataAsJson<JsonLdFormatDataVerifiableCredential>(); + const w3cCredential = JsonTransformer.fromJSON( + credentialJson, + W3cJsonLdVerifiableCredential, + ); + const request = + requestAttachment.getDataAsJson<JsonLdFormatDataCredentialDetail>(); + + try { + // This check is also done in the processCredential method, but we do it here as well + // to be certain we don't skip the check + this.verifyReceivedCredentialMatchesRequest(w3cCredential, request); + + return true; + } catch (error) { + return false; + } + } + + /** + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + */ + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: "application/json", + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }); + + return attachment; + } +} diff --git a/src/screens/CredentialDetails/CredentialDetailsStore.ts b/src/screens/CredentialDetails/CredentialDetailsStore.ts index 400048b04f7b31aa3cd2017c3cc0d79cf866a3af..3c1741029cc632c022a7d97ed43f0270ad652cd0 100644 --- a/src/screens/CredentialDetails/CredentialDetailsStore.ts +++ b/src/screens/CredentialDetails/CredentialDetailsStore.ts @@ -1,6 +1,6 @@ import {makeAutoObservable} from "mobx"; import rootStore from "src/store/rootStore"; -import {CredentialState, ProofState} from "@aries-framework/core"; +import {CredentialState, GetCredentialFormatDataReturn, ProofState} from "@aries-framework/core"; import {RecordHistory} from "src/type/record"; import { CredentialExchangeRecord @@ -13,6 +13,7 @@ class CredentialDetailsStore { public loading = true; public credentialId: string; private _credential: CredentialExchangeRecord | null = null; + public credentialFormatData: GetCredentialFormatDataReturn | null = null; public get credential(): CredentialExchangeRecord { return this._credential as CredentialExchangeRecord; } @@ -28,6 +29,7 @@ class CredentialDetailsStore { this.credentialId = credentialId; this.history = []; this._credential = await rootStore.agentStore.agent.credentials.getById(credentialId); + this.credentialFormatData = await rootStore.agentStore.agent.credentials.getFormatData(credentialId); await this._loadHistory(); this.loading = false; }; diff --git a/src/screens/CredentialDetails/index.tsx b/src/screens/CredentialDetails/index.tsx index 0004a1f77e8b38a8a5ecc002c76408ee3f223f13..59a44b38843e191dd2711421083c1b2c53a04c32 100644 --- a/src/screens/CredentialDetails/index.tsx +++ b/src/screens/CredentialDetails/index.tsx @@ -13,6 +13,7 @@ import InfoSvg from 'src/assets/svg/info.svg'; import ActivitiesSvg from 'src/assets/svg/activities.svg'; import CredentialDetailsStore from "./CredentialDetailsStore"; import Loader from "src/components/Loader"; +import JsonLdCredentialViewer from "src/components/jsonld/JsonLdCredentialViewer"; type CredentialDetailsProps = StackScreenProps< CredentialStackParams, @@ -52,6 +53,9 @@ const CredentialDetails: React.FC<CredentialDetailsProps> = observer(({ ); } + + const jsonldCredential = store.credentialFormatData?.credential?.jsonld as any || null; + return ( <ScrollView contentContainerStyle={styles.scrollView}> <StatusBar barStyle="light-content" /> @@ -82,9 +86,13 @@ const CredentialDetails: React.FC<CredentialDetailsProps> = observer(({ </View> ); })} + {!!jsonldCredential && ( + <JsonLdCredentialViewer jsonld={jsonldCredential} /> + )} </View> </View> + <View style={styles.card}> <View style={styles.header}> <ActivitiesSvg style={styles.headerIcon} /> @@ -181,4 +189,8 @@ const styles = StyleSheet.create({ primaryColor: { color: ColorPallet.brand.primary, }, + jsonld: { + ...TextTheme.normal, + color: ColorPallet.baseColors.black + }, }); diff --git a/src/screens/CredentialOffer/index.tsx b/src/screens/CredentialOffer/index.tsx index 2588e8330b51bcad390f86d503c99aeed4cf9ce2..2e9c77494c0d822a2ab1a093d0f95f6ec7dd8ac5 100644 --- a/src/screens/CredentialOffer/index.tsx +++ b/src/screens/CredentialOffer/index.tsx @@ -17,6 +17,7 @@ import Button, {ButtonType} from "src/components/Button"; import Record from "src/components/Record"; import Title from "src/components/Title"; import {CredentialState} from "@aries-framework/core"; +import JsonLdCredentialViewer from "src/components/jsonld/JsonLdCredentialViewer"; type CredentialOfferProps = StackScreenProps< NotificationStackParams, @@ -66,8 +67,10 @@ const CredentialOffer: React.FC<CredentialOfferProps> = observer(({ } }, [credential.state, connection]); - const redirectToHome = () => navigation.getParent()?.navigate(Screens.Credentials); + const jsonLdData = store.credentialRecord?.offer?.jsonld as any || null; + const jsonldCredential = jsonLdData?.credential; + return ( <> <StatusBar barStyle="light-content" /> @@ -89,6 +92,11 @@ const CredentialOffer: React.FC<CredentialOfferProps> = observer(({ <CredentialCard credential={credential} /> )} </View> + {!!jsonldCredential && ( + <View style={styles.jsonldWrapper}> + <JsonLdCredentialViewer jsonld={jsonldCredential} /> + </View> + )} </> )} footer={() => ( @@ -183,4 +191,7 @@ const styles = StyleSheet.create({ paddingTop: 10, backgroundColor: ColorPallet.grayscale.white, }, + jsonldWrapper: { + paddingHorizontal: 16, + } }); diff --git a/src/store/AgentStore.ts b/src/store/AgentStore.ts index 41f4a2a1d364f37b7c1518911efeb007a1fcece7..2e38386e67dc9c01d7d705b9ce0f3e7b26e3b763 100644 --- a/src/store/AgentStore.ts +++ b/src/store/AgentStore.ts @@ -22,6 +22,8 @@ import { ProofsModule, V2CredentialProtocol, V2ProofProtocol, + W3cCredentialService, + W3cCredentialsModule, WsOutboundTransport, } from "@aries-framework/core"; import { agentDependencies } from "@aries-framework/react-native"; @@ -79,6 +81,7 @@ import TrustedEmailSender from "../db-models/TrustedEmailSender"; import axios from "axios"; import { NativeModules } from "react-native"; import { addHours } from "date-fns"; +import { JsonLdCredentialFormatService } from "../credo/JsonLdCredentialFormatService"; const { VereignImapModule } = NativeModules; @@ -162,6 +165,7 @@ class AgentStore { credentialFormats: [ legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService(), + new JsonLdCredentialFormatService() ], }), ], @@ -211,6 +215,7 @@ class AgentStore { // mediatorPickupStrategy: MediatorPickupStrategy.Implicit, mediatorInvitationUrl: Config.MEDIATOR_URL, }), + w3c: new W3cCredentialsModule() }; const newAgent = new Agent({ @@ -219,6 +224,12 @@ class AgentStore { modules, }); + const w3cCredentialService = newAgent.context.dependencyManager.resolve(W3cCredentialService); + // TODO Hack dor demo purpose + // @ts-ignore + w3cCredentialService.verifyCredential = () => ({ isValid: true }) + + newAgent.registerOutboundTransport(new WsOutboundTransport()); newAgent.registerOutboundTransport(new HttpOutboundTransport()); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index b5ebca362f5f5044a78d50f59e5eaca5aa2af32f..085cfd8139199a6c1bffb661fbd7185cda65187b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -78,6 +78,9 @@ export function parsedCredentialDefinition( } export function hashCode(s: string): number { + if (!s) { + return 0; + } return s .split('') .reduce((hash, char) => char.charCodeAt(0) + ((hash << 5) - hash), 0);