diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index ac5cde9..35c428f 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -26,6 +26,7 @@ export type Settings = { lastName?: string; // deprecated - put all names in firstName lastNotifiedClaimId?: string; lastViewedClaimId?: string; + passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes profileImageUrl?: string; reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders reminderOn?: boolean; // Toggle to enable or disable reminders @@ -46,7 +47,7 @@ export type Settings = { }; export function isAnyFeedFilterOn(settings: Settings): boolean { - return !!(settings.filterFeedByNearby || settings.filterFeedByVisible); + return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible); } /** @@ -60,3 +61,5 @@ export const SettingsSchema = { * Constants. */ export const MASTER_SETTINGS_KEY = 1; + +export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15; diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 862ed54..2ac8aef 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -85,7 +85,7 @@ export const generateSeed = (): string => { }; /** - * Retreive an access token + * Retrieve an access token, or "" if no DID is provided. * * @return {*} */ diff --git a/src/libs/crypto/vc/passkeyDidPeer.ts b/src/libs/crypto/vc/passkeyDidPeer.ts index 920d751..5efc372 100644 --- a/src/libs/crypto/vc/passkeyDidPeer.ts +++ b/src/libs/crypto/vc/passkeyDidPeer.ts @@ -411,13 +411,21 @@ async function peerDidToDidDocument(did: string): Promise { } // this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types // (another reference is the @aviarytech/did-peer resolver) + + /** + * Looks like JsonWebKey2020 isn't too difficult: + * - change context security/suites link to jws-2020/v1 + * - change publicKeyMultibase to publicKeyJwk generated with cborToKeys + * - change type to JsonWebKey2020 + */ + const id = did.split(":")[2]; const multibase = id.slice(1); const encnumbasis = multibase.slice(1); const didDocument = { "@context": [ "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1", ], assertionMethod: [did + "#" + encnumbasis], authentication: [did + "#" + encnumbasis], diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 26954cd..754766b 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -5,9 +5,10 @@ import * as R from "ramda"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { Contact } from "@/db/tables/contacts"; import { accessToken } from "@/libs/crypto"; -import { NonsensitiveDexie } from "@/db/index"; -import { getAccount } from "@/libs/util"; +import { db, NonsensitiveDexie } from "@/db/index"; +import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util"; import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc"; +import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; // the object in RegisterAction claims @@ -447,12 +448,57 @@ export function didInfo( return didInfoForContact(did, activeDid, contact, allMyDids).displayName; } +let passkeyAccessToken: string = ""; +let passkeyTokenExpirationEpochSeconds: number = 0; + +export function clearPasskeyToken() { + passkeyAccessToken = ""; + passkeyTokenExpirationEpochSeconds = 0; +} + +export function tokenExpiryTimeDescription() { + if ( + !passkeyAccessToken || + passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000 + ) { + return "Token has expired"; + } else { + return ( + "Token expires at " + + new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString() + ); + } +} + +/** + * Get the headers for a request, potentially including Authorization + */ export async function getHeaders(did?: string) { const headers: { "Content-Type": string; Authorization?: string } = { "Content-Type": "application/json", }; if (did) { - const token = await accessToken(did); + let token; + const account = await getAccount(did); + if (account?.passkeyCredIdHex) { + if ( + passkeyAccessToken && + passkeyTokenExpirationEpochSeconds > Date.now() / 1000 + ) { + // there's an active current passkey token + token = passkeyAccessToken; + } else { + // there's no current passkey token or it's expired + token = await accessToken(did); + + passkeyAccessToken = token; + const passkeyExpirationSeconds = await getPasskeyExpirationSeconds(); + passkeyTokenExpirationEpochSeconds = + Date.now() / 1000 + passkeyExpirationSeconds; + } + } else { + token = await accessToken(did); + } headers["Authorization"] = "Bearer " + token; } else { // it's often OK to request without auth; we assume necessary checks are done earlier diff --git a/src/libs/util.ts b/src/libs/util.ts index c0292ac..c652683 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -1,21 +1,20 @@ // many of these are also found in endorser-mobile utility.ts import axios, { AxiosResponse } from "axios"; -import { IIdentifier } from "@veramo/core"; import { useClipboard } from "@vueuse/core"; import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { Account } from "@/db/tables/accounts"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import {DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY} from "@/db/tables/settings"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer"; import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; import { Buffer } from "buffer"; -import {KeyMeta} from "@/libs/crypto/vc"; -import {createPeerDid} from "@/libs/crypto/vc/didPeer"; +import { KeyMeta } from "@/libs/crypto/vc"; +import { createPeerDid } from "@/libs/crypto/vc/didPeer"; export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; @@ -273,6 +272,15 @@ export const registerSaveAndActivatePasskey = async ( return account; }; +export const getPasskeyExpirationSeconds = async (): Promise => { + await db.open(); + const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const passkeyExpirationSeconds = + (settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) * + 60; + return passkeyExpirationSeconds; +}; + export const sendTestThroughPushServer = async ( subscriptionJSON: PushSubscriptionJSON, skipFilter: boolean, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index d32267e..2f99791 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -152,7 +152,7 @@
- Activity + Your Activity
@@ -216,7 +216,6 @@
Location
Set Search Area… @@ -622,6 +621,26 @@ +
+ + + Passkey Expiration Minutes + +
+ + {{ passkeyExpirationDescription }} + +
+
+ +
+
+