From c200cdbead1a2dd8423babc26f6b20da1f876917 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Tue, 9 Jul 2024 17:56:48 -0600 Subject: [PATCH] add expiration inside JWANT & refactor getHeaders to move toward supporting did:peer --- src/libs/crypto/index.ts | 49 ++++++++++++------- src/libs/crypto/passkeyHelpers.ts | 4 +- src/libs/didPeer.ts | 29 +++++++++-- src/libs/endorserServer.ts | 76 +++++++++++++++-------------- src/libs/util.ts | 7 ++- src/views/AccountViewView.vue | 39 +++------------ src/views/ClaimAddRawView.vue | 14 ------ src/views/ClaimView.vue | 40 ++++----------- src/views/ConfirmGiftView.vue | 27 +++------- src/views/ContactAmountsView.vue | 20 +++----- src/views/ContactGiftingView.vue | 31 +----------- src/views/ContactQRScanShowView.vue | 7 ++- src/views/ContactsView.vue | 36 ++++++-------- src/views/GiftedDetails.vue | 2 +- src/views/HomeView.vue | 40 ++------------- src/views/NewEditProjectView.vue | 22 +++------ src/views/TestView.vue | 4 +- 17 files changed, 167 insertions(+), 280 deletions(-) diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 71c67fed..ee630124 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -6,7 +6,8 @@ import { HDNode } from "@ethersproject/hdnode"; import * as didJwt from "did-jwt"; import * as u8a from "uint8arrays"; -import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer"; +import { Account } from "@/db/tables/accounts"; +import { createEndorserJwt, ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; @@ -88,23 +89,35 @@ export const generateSeed = (): string => { * @param {IIdentifier} identifier * @return {*} */ -export const accessToken = async (identifier: IIdentifier) => { - const did: string = identifier.did; - const privateKeyHex: string = identifier.keys[0].privateKeyHex as string; - - const signer = SimpleSigner(privateKeyHex); - - const nowEpoch = Math.floor(Date.now() / 1000); - const endEpoch = nowEpoch + 60; // add one minute - - const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; - const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R - const jwt: string = await didJwt.createJWT(tokenPayload, { - alg, - issuer: did, - signer, - }); - return jwt; +export const accessToken = async ( + identifier: IIdentifier | undefined, + did?: string, +) => { + if (did) { + const nowEpoch = Math.floor(Date.now() / 1000); + const endEpoch = nowEpoch + 60; // add one minute + const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; + return createEndorserJwt(did, tokenPayload); + } else { + // deprecated + // must have identifier + const did = identifier?.did; + const privateKeyHex: string = identifier?.keys[0].privateKeyHex as string; + + const signer = SimpleSigner(privateKeyHex); + + const nowEpoch = Math.floor(Date.now() / 1000); + const endEpoch = nowEpoch + 60; // add one minute + + const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; + const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R + const jwt: string = await didJwt.createJWT(tokenPayload, { + alg, + issuer: did || "no DID set", + signer, + }); + return jwt; + } }; export const sign = async (privateKeyHex: string) => { diff --git a/src/libs/crypto/passkeyHelpers.ts b/src/libs/crypto/passkeyHelpers.ts index 5e3daabe..59f6b983 100644 --- a/src/libs/crypto/passkeyHelpers.ts +++ b/src/libs/crypto/passkeyHelpers.ts @@ -88,7 +88,7 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> { ); return toResolve; } -export class MissingWebCrypto extends Error { +class MissingWebCrypto extends Error { constructor() { const message = "An instance of the Crypto API could not be located"; super(message); @@ -96,7 +96,7 @@ export class MissingWebCrypto extends Error { } } // Make it possible to stub return values during testing -export const _getWebCryptoInternals = { +const _getWebCryptoInternals = { stubThisGlobalThisCrypto: () => globalThis.crypto, // Make it possible to reset the `webCrypto` at the top of the file setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => { diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index 7a90c80f..acc0fbf2 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -117,13 +117,17 @@ export class PeerSetup { issuerDid: string, payload: object, credIdHex: string, + expMinutes: number = 1, ) { const credentialId = arrayBufferToBase64URLString( Buffer.from(credIdHex, "hex").buffer, ); + const issuedAt = Math.floor(Date.now() / 1000); + const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now const fullPayload = { ...payload, - iat: Math.floor(Date.now() / 1000), + exp: expiryTime, + iat: issuedAt, iss: issuerDid, }; this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); @@ -159,7 +163,8 @@ export class PeerSetup { const dataInJwt = { AuthenticationDataB64URL: authenticatorDataBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url, - iat: Math.floor(Date.now() / 1000), + exp: expiryTime, + iat: issuedAt, iss: issuerDid, }; const dataInJwtString = JSON.stringify(dataInJwt); @@ -178,10 +183,14 @@ export class PeerSetup { issuerDid: string, payload: object, credIdHex: string, + expMinutes: number = 1, ) { + const issuedAt = Math.floor(Date.now() / 1000); + const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now const fullPayload = { ...payload, - iat: Math.floor(Date.now() / 1000), + exp: expiryTime, + iat: issuedAt, iss: issuerDid, }; const dataToSignString = JSON.stringify(fullPayload); @@ -227,7 +236,8 @@ export class PeerSetup { const dataInJwt = { AuthenticationDataB64URL: authenticatorDataBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url, - iat: Math.floor(Date.now() / 1000), + exp: expiryTime, + iat: issuedAt, iss: issuerDid, }; const dataInJwtString = JSON.stringify(dataInJwt); @@ -308,6 +318,16 @@ export class PeerSetup { // } } +export async function createDidPeerJwt( + did: string, + credIdHex: string, + payload: object, +): Promise { + const peerSetup = new PeerSetup(); + const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex); + return jwt; +} + // I'd love to use this but it doesn't verify. // Requires: // npm install @noble/curves @@ -380,6 +400,7 @@ export async function verifyJwtSimplewebauthn( return verification.verified; } +// similar code is in endorser-ch util-crypto.ts verifyPeerSignature export async function verifyJwtWebCrypto( credId: Base64URLString, issuerDid: string, diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 835fe5d8..e4894acf 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -2,7 +2,6 @@ import { Axios, AxiosRequestConfig, AxiosResponse, - RawAxiosRequestHeaders, } from "axios"; import * as didJwt from "did-jwt"; import { LRUCache } from "lru-cache"; @@ -13,7 +12,8 @@ import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { Contact } from "@/db/tables/contacts"; import { accessToken, SimpleSigner } from "@/libs/crypto"; import { NonsensitiveDexie } from "@/db/index"; -import { getIdentity } from "@/libs/util"; +import { createDidPeerJwt } from "@/libs/didPeer"; +import { getAccount, getIdentity } from "@/libs/util"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; // the object in RegisterAction claims @@ -160,7 +160,7 @@ export interface OfferVerifiableCredential { // Note that previous VCs may have additional fields. // https://endorser.ch/doc/html/transactions.html#id7 export interface PlanVerifiableCredential { - "@context": SCHEMA_ORG_CONTEXT; + "@context": "https://schema.org"; "@type": "PlanAction"; name: string; agent?: { identifier: string }; @@ -453,28 +453,30 @@ export function didInfo( return didInfoForContact(did, activeDid, contact, allMyDids).displayName; } -async function getHeaders(identity: IIdentifier | null) { - const headers: RawAxiosRequestHeaders = { +export async function getHeaders(did?: string) { + const headers: { "Content-Type": string; Authorization?: string } = { "Content-Type": "application/json", }; - if (identity) { - const token = await accessToken(identity); + if (did) { + const token = await accessToken(undefined, did); headers["Authorization"] = "Bearer " + token; + } else { + // it's often OK to request without auth; we assume necessary checks are done earlier } return headers; } /** * @param handleId nullable, in which case "undefined" will be returned - * @param identity nullable, in which case no private info will be returned + * @param requesterDid optional, in which case no private info will be returned * @param axios * @param apiServer */ export async function getPlanFromCache( handleId: string | null, - identity: IIdentifier | null, axios: Axios, apiServer: string, + requesterDid?: string, ): Promise { if (!handleId) { return undefined; @@ -485,7 +487,7 @@ export async function getPlanFromCache( apiServer + "/api/v2/report/plans?handleId=" + encodeURIComponent(handleId); - const headers = await getHeaders(identity); + const headers = await getHeaders(requesterDid); try { const resp = await axios.get(url, { headers }); if (resp.status === 200 && resp.data?.data?.length > 0) { @@ -944,18 +946,34 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => { }; }; +export async function createEndorserJwt(did: string, payload: object) { + const account = await getAccount(did); + if (account.identity) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const identity = JSON.parse(account.identity!); + const privateKeyHex = identity.keys[0].privateKeyHex; + const signer = await SimpleSigner(privateKeyHex); + return didJwt.createJWT(payload, { + issuer: did, + signer: signer, + }); + } else if (account.passkeyCredIdHex) { + return createDidPeerJwt(did, account.passkeyCredIdHex, payload); + } else { + throw new Error("No identity data found to sign for DID " + did); + } +} + export async function register( activeDid: string, apiServer: string, axios: Axios, contact: Contact, ) { - const identity = await getIdentity(activeDid); - const vcClaim: RegisterVerifiableCredential = { "@context": SCHEMA_ORG_CONTEXT, "@type": "RegisterAction", - agent: { identifier: identity.did }, + agent: { identifier: activeDid }, object: SERVICE_ID, participant: { identifier: contact.did }, }; @@ -968,26 +986,10 @@ export async function register( }, }; // Create a signature using private key of identity - if (identity.keys[0].privateKeyHex == null) { - return { error: "Private key not found." }; - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const privateKeyHex: string = identity.keys[0].privateKeyHex!; - const signer = await SimpleSigner(privateKeyHex); - const alg = undefined; - // Create a JWT for the request - const vcJwt: string = await didJwt.createJWT(vcPayload, { - alg: alg, - issuer: identity.did, - signer: signer, - }); - - // Make the xhr request payload - const payload = JSON.stringify({ jwtEncoded: vcJwt }); - const url = apiServer + "/api/v2/claim"; - const headers = await getHeaders(identity); + const vcJwt = await createEndorserJwt(activeDid, vcPayload); - const resp = await axios.post(url, payload, { headers }); + const url = apiServer + "/api/v2/claim"; + const resp = await axios.post(url, { jwtEncoded: vcJwt }); if (resp.data?.success?.handleId) { return { success: true }; } else if (resp.data?.success?.embeddedRecordError) { @@ -1017,7 +1019,7 @@ export async function setVisibilityUtil( const url = apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe"); const identity = await getIdentity(activeDid); - const headers = await getHeaders(identity); + const headers = await getHeaders(identity.did); const payload = JSON.stringify({ did: contact.did }); try { @@ -1052,10 +1054,10 @@ export async function setVisibilityUtil( export async function fetchEndorserRateLimits( apiServer: string, axios: Axios, - identity: IIdentifier, + did: string, ) { const url = `${apiServer}/api/report/rateLimits`; - const headers = await getHeaders(identity); + const headers = await getHeaders(did); return await axios.get(url, { headers } as AxiosRequestConfig); } @@ -1070,9 +1072,9 @@ export async function fetchEndorserRateLimits( export async function fetchImageRateLimits( apiServer: string, axios: Axios, - identity: IIdentifier, + did: string, ) { const url = DEFAULT_IMAGE_API_SERVER + "/image-limits"; - const headers = await getHeaders(identity); + const headers = await getHeaders(did); return await axios.get(url, { headers } as AxiosRequestConfig); } diff --git a/src/libs/util.ts b/src/libs/util.ts index e4bb9f91..54dd6262 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -196,12 +196,17 @@ export function findAllVisibleToDids( * **/ -export const getIdentity = async (activeDid: string): Promise => { +export const getAccount = async (activeDid: string): Promise => { await accountsDB.open(); const account = (await accountsDB.accounts .where("did") .equals(activeDid) .first()) as Account; + return account; +}; + +export const getIdentity = async (activeDid: string): Promise => { + const account = await getAccount(activeDid); const identity = JSON.parse(account?.identity || "null"); if (!identity) { diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 98fbf5b2..60e046f3 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -807,32 +807,6 @@ export default class AccountViewView extends Vue { } } - /** - * Asynchronously retrieves headers for HTTP requests. - * - * @param {IIdentifier} identity - The identity object for which to generate the headers. - * @returns {Promise>} A Promise that resolves to an object containing the headers. - * - * @throws Will throw an error if unable to generate an access token. - */ - public async getHeaders( - identity: IIdentifier, - ): Promise> { - try { - const token = await accessToken(identity); - - const headers: Record = { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }; - - return headers; - } catch (error) { - console.error("Failed to get headers:", error); - return Promise.reject(error); - } - } - // call fn, copy text to the clipboard, then redo fn after 2 seconds doCopyTwoSecRedo(text: string, fn: () => void) { fn(); @@ -884,7 +858,7 @@ export default class AccountViewView extends Vue { this.publicHex = identity.keys[0].publicKeyHex; this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.derivationPath = identity.keys[0].meta?.derivationPath as string; - this.checkLimitsFor(identity); + this.checkLimitsFor(this.activeDid); } else { // Handle the case where any of these are null or undefined } @@ -1238,9 +1212,8 @@ export default class AccountViewView extends Vue { } async checkLimits() { - const identity = await this.getIdentity(this.activeDid); - if (identity) { - this.checkLimitsFor(identity); + if (this.activeDid) { + this.checkLimitsFor(this.activeDid); } else { this.limitsMessage = "You have no identifier, or your data has been corrupted."; @@ -1252,7 +1225,7 @@ export default class AccountViewView extends Vue { * * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`. */ - public async checkLimitsFor(identity: IIdentifier) { + public async checkLimitsFor(did: string) { this.loadingLimits = true; this.limitsMessage = ""; @@ -1260,7 +1233,7 @@ export default class AccountViewView extends Vue { const resp = await fetchEndorserRateLimits( this.apiServer, this.axios, - identity, + did, ); if (resp.status === 200) { this.endorserLimits = resp.data; @@ -1288,7 +1261,7 @@ export default class AccountViewView extends Vue { const imageResp = await fetchImageRateLimits( this.apiServer, this.axios, - identity, + did, ); if (imageResp.status === 200) { this.imageLimits = imageResp.data; diff --git a/src/views/ClaimAddRawView.vue b/src/views/ClaimAddRawView.vue index 4655d145..57c98d7c 100644 --- a/src/views/ClaimAddRawView.vue +++ b/src/views/ClaimAddRawView.vue @@ -29,7 +29,6 @@