diff --git a/src/libs/crypto/vc/index.ts b/src/libs/crypto/vc/index.ts index 889103bcc..252ca06df 100644 --- a/src/libs/crypto/vc/index.ts +++ b/src/libs/crypto/vc/index.ts @@ -9,7 +9,6 @@ import { Buffer } from "buffer/"; import * as didJwt from "did-jwt"; import { JWTVerified } from "did-jwt"; -import { JWTDecoded } from "did-jwt/lib/JWT"; import { Resolver } from "did-resolver"; import { IIdentifier } from "@veramo/core"; import * as u8a from "uint8arrays"; @@ -41,7 +40,7 @@ export interface KeyMeta { passkeyCredIdHex?: string; } -const resolver = new Resolver({ ethr: didEthLocalResolver }); +const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver }); /** * Tell whether a key is from a passkey @@ -62,6 +61,7 @@ export async function createEndorserJwtForKey( const privateKeyHex = identity.keys[0].privateKeyHex; const signer = await SimpleSigner(privateKeyHex as string); const options = { + // alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests issuer: account.did, signer: signer, expiresIn: undefined as number | undefined, @@ -124,7 +124,8 @@ function bytesToHex(b: Uint8Array): string { } // We should be calling 'verify' in more places, showing warnings if it fails. -export function decodeEndorserJwt(jwt: string): JWTDecoded { +// @returns JWTDecoded with { header: JWTHeader, payload: string, signature: string, data: string } (but doesn't verify the signature) +export function decodeEndorserJwt(jwt: string) { return didJwt.decodeJWT(jwt); } @@ -134,10 +135,8 @@ export async function decodeAndVerifyJwt( jwt: string, ): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> { const pieces = jwt.split("."); - console.log("WTF decodeAndVerifyJwt", typeof jwt, jwt, pieces); const header = JSON.parse(base64urlDecodeString(pieces[0])); const payload = JSON.parse(base64urlDecodeString(pieces[1])); - console.log("WTF decodeAndVerifyJwt after", header, payload); const issuerDid = payload.iss; if (!issuerDid) { return Promise.reject({ @@ -149,7 +148,9 @@ export async function decodeAndVerifyJwt( if (issuerDid.startsWith(ETHR_DID_PREFIX)) { try { - const verified = await didJwt.verifyJWT(jwt, { resolver }); + const verified = await didJwt.verifyJWT(jwt, { + resolver: ethLocalResolver, + }); return verified; } catch (e: unknown) { return Promise.reject({ diff --git a/src/router/index.ts b/src/router/index.ts index 5c198c3cc..ba4b3a244 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -70,7 +70,7 @@ const routes: Array<RouteRecordRaw> = [ component: () => import("../views/ContactGiftingView.vue"), }, { - path: "/contact-import", + path: "/contact-import/:jwt?", name: "contact-import", component: () => import("../views/ContactImportView.vue"), }, diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue index b7ec1ba1d..9a7a5cb3d 100644 --- a/src/views/ContactImportView.vue +++ b/src/views/ContactImportView.vue @@ -16,10 +16,11 @@ Contact Import </h1> - <span class="flex justify-center"> + <span v-if="contactsImporting.length > 0" class="flex justify-center"> <input type="checkbox" v-model="makeVisible" class="mr-2" /> Make my activity visible to these contacts. </span> + <div v-if="sameCount > 0"> <span v-if="sameCount == 1" >One contact is the same as an existing contact</span @@ -85,17 +86,19 @@ </template> <script lang="ts"> +import { JWTVerified } from "did-jwt"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; +import QuickNav from "@/components/QuickNav.vue"; +import EntityIcon from "@/components/EntityIcon.vue"; +import OfferDialog from "@/components/OfferDialog.vue"; import { AppString, NotificationIface } from "@/constants/app"; import { db, retrieveSettingsForActiveAccount } from "@/db/index"; import { Contact } from "@/db/tables/contacts"; import * as libsUtil from "@/libs/util"; -import QuickNav from "@/components/QuickNav.vue"; -import EntityIcon from "@/components/EntityIcon.vue"; -import OfferDialog from "@/components/OfferDialog.vue"; +import { decodeAndVerifyJwt } from "@/libs/crypto/vc/index"; import { setVisibilityUtil } from "@/libs/endorserServer"; @Component({ @@ -127,9 +130,27 @@ export default class ContactImportView extends Vue { this.apiServer = settings.apiServer || ""; // Retrieve the imported contacts from the query parameter - const importedContacts = - ((this.$route as Router).query["contacts"] as string) || "[]"; - this.contactsImporting = JSON.parse(importedContacts); + const importedContacts = (this.$route as Router).query[ + "contacts" + ] as string; + if (importedContacts) { + await this.setContactsSelected(JSON.parse(importedContacts)); + } + + // match everything after /contact-import/ in the window.location.pathname + const jwt = window.location.pathname.match( + /\/contact-import\/(ey.+)$/, + )?.[1]; + if (jwt) { + // decode the JWT + // eslint-disable-next-line prettier/prettier + const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt); + await this.setContactsSelected(parsedJwt.payload.contacts as Contact[]); + } + } + + async setContactsSelected(contacts: Array<Contact>) { + this.contactsImporting = contacts; this.contactsSelected = new Array(this.contactsImporting.length).fill(true); await db.open(); diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index d82bd6e54..75761cf27 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -323,7 +323,7 @@ import GiftedDialog from "@/components/GiftedDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue"; import ContactNameDialog from "@/components/ContactNameDialog.vue"; import TopMessage from "@/components/TopMessage.vue"; -import { AppString, NotificationIface } from "@/constants/app"; +import { APP_SERVER, AppString, NotificationIface } from "@/constants/app"; import { db, retrieveSettingsForActiveAccount, @@ -336,6 +336,7 @@ import { decodeEndorserJwt } from "@/libs/crypto/vc"; import { CONTACT_CSV_HEADER, CONTACT_URL_PREFIX, + createEndorserJwtForDid, GiveSummaryRecord, getHeaders, isDid, @@ -415,6 +416,7 @@ export default class ContactsView extends Vue { ); // handle a contact sent via URL + // @deprecated: use /contact-import/:jwt with a JWT that has an array of contacts const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded) .query["contactJwt"] as string; if (importedContactJwt) { @@ -1247,7 +1249,7 @@ export default class ContactsView extends Vue { }; } - private copySelectedContacts() { + private async copySelectedContacts() { if (this.contactsSelected.length === 0) { this.danger("You must select contacts to copy."); return; @@ -1255,18 +1257,23 @@ export default class ContactsView extends Vue { const selectedContacts = this.contacts.filter((c) => this.contactsSelected.includes(c.did), ); - const message = - "To add contacts, paste this into the box on the 'Contacts' screen.\n\n" + - JSON.stringify(selectedContacts); + console.log( + "Array of selected contacts:", + JSON.stringify(selectedContacts), + ); + const contactsJwt = await createEndorserJwtForDid(this.activeDid, { + contacts: selectedContacts, + }); + const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt; useClipboard() - .copy(message) + .copy(contactsJwtUrl) .then(() => { this.$notify( { group: "alert", type: "info", title: "Copied", - text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.", + text: "The link for those contacts is now in the clipboard.", }, 5000, ); diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index 3248ff87f..465c5be59 100644 --- a/src/views/GiftedDetailsView.vue +++ b/src/views/GiftedDetailsView.vue @@ -114,7 +114,7 @@ {{ giverDid ? "This was provided by " + giverName + "." - : "No individual gave." + : "No named individual gave." }} </label> <fa diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 43922034f..5e4630e17 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -188,7 +188,7 @@ export default class IdentitySwitcherView extends Vue { group: "alert", type: "warning", title: "Cannot Delete", - text: "You cannot delete the active identity.", + text: "You cannot delete the active identity. Set to another identity or 'no identity' first.", }, 3000, );