From 2dd6e9b07a3093ce4667d9336a5a2a97d3be70f1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 6 Jul 2024 19:12:31 -0600 Subject: [PATCH 1/7] make a passkey-generator in start & home pages, and make that the default --- src/libs/didPeer.ts | 5 +- src/libs/util.ts | 35 ++++++ src/views/GiftedDetails.vue | 7 +- src/views/HomeView.vue | 206 +++++++++++++++++++----------------- src/views/StartView.vue | 79 ++++++++++---- src/views/TestView.vue | 24 ++--- 6 files changed, 220 insertions(+), 136 deletions(-) diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index 74c8b191a..7a90c80fa 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -20,6 +20,7 @@ import { PublicKeyCredentialRequestOptionsJSON, } from "@simplewebauthn/types"; +import { AppString } from "@/constants/app"; import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; const PEER_DID_PREFIX = "did:peer:"; @@ -42,9 +43,9 @@ function arrayToBase64Url(anything: Uint8Array) { export async function registerCredential(passkeyName?: string) { const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({ - rpName: "Time Safari", + rpName: AppString.APP_NAME, rpID: window.location.hostname, - userName: passkeyName || "Time Safari User", + userName: passkeyName || AppString.APP_NAME + " User", // Don't prompt users for additional information about the authenticator // (Recommended for smoother UX) attestationType: "none", diff --git a/src/libs/util.ts b/src/libs/util.ts index cdecf3173..e4bb9f91e 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -11,6 +11,9 @@ import { 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 { createPeerDid, registerCredential } from "@/libs/didPeer"; + +import { Buffer } from "buffer"; export const PRIVACY_MESSAGE = "The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow."; @@ -239,6 +242,38 @@ export const generateSaveAndActivateIdentity = async (): Promise => { return newId.did; }; +export const registerAndSavePasskey = async ( + keyName: string, +): Promise => { + const cred = await registerCredential(keyName); + const publicKeyBytes = cred.publicKeyBytes; + const did = createPeerDid(publicKeyBytes as Uint8Array); + const passkeyCredIdHex = cred.credIdHex as string; + + const account = { + dateCreated: new Date().toISOString(), + did, + passkeyCredIdHex, + publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"), + }; + await accountsDB.open(); + await accountsDB.accounts.add(account); + return account; +}; + +export const registerSaveAndActivatePasskey = async ( + keyName: string, +): Promise => { + const account = await registerAndSavePasskey(keyName); + + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + activeDid: account.did, + }); + + return account; +}; + export const sendTestThroughPushServer = async ( subscriptionJSON: PushSubscriptionJSON, skipFilter: boolean, diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index 48d871905..f05ddd2f7 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -180,16 +180,17 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; import QuickNav from "@/components/QuickNav.vue"; import TopMessage from "@/components/TopMessage.vue"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; -import {accountsDB, db} from "@/db/index"; +import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { constructGive, - createAndSubmitGive, didInfo, + createAndSubmitGive, + didInfo, getPlanFromCache, } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { accessToken } from "@/libs/crypto"; -import {Contact} from "@/db/tables/contacts"; +import { Contact } from "@/db/tables/contacts"; @Component({ components: { diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index e91c16cd6..c628cb59e 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -5,7 +5,7 @@

- Time Safari + {{ AppString.APP_NAME }}

@@ -79,89 +79,100 @@

- Want to connect with your contacts, or share contributions or - projects? + Want to see info from your contacts, or share contributions?

- - Create An Identifier - -
- -
- - Someone must register you before you can give kudos or make offers or - create projects... basically before doing anything. - - Show Them Your Identifier Info - +
+ + + Give me all the options. + +
-
- - - -
-

Record Something Given By:

-
+
+ -
    -
  • - -

    - Unnamed/Unknown -

    -
  • -
  • - -

    - {{ contact.name || contact.did }} -

    -
  • -
- -
+ + Someone must register you before you can give kudos or make offers + or create projects... basically before doing anything. - Choose From All Contacts + Show Them Your Identifier Info - +
  • + +

    + Unnamed/Unknown +

    +
  • +
  • + +

    + {{ contact.name || contact.did }} +

    +
  • + + +
    + + Choose From All Contacts + + +
    @@ -309,6 +320,7 @@ import { IIdentifier } from "@veramo/core"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; +import App from "../App.vue"; import EntityIcon from "@/components/EntityIcon.vue"; import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue"; @@ -316,7 +328,7 @@ import FeedFilters from "@/components/FeedFilters.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue"; import QuickNav from "@/components/QuickNav.vue"; import TopMessage from "@/components/TopMessage.vue"; -import { NotificationIface } from "@/constants/app"; +import { AppString, NotificationIface } from "@/constants/app"; import { db, accountsDB } from "@/db/index"; import { Account } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; @@ -336,7 +348,7 @@ import { GiverReceiverInputInfo, GiveSummaryRecord, } from "@/libs/endorserServer"; -import { generateSaveAndActivateIdentity } from "@/libs/util"; +import { registerSaveAndActivatePasskey } from "@/libs/util"; interface GiveRecordWithContactInfo extends GiveSummaryRecord { giver: { @@ -354,6 +366,11 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord { } @Component({ + computed: { + App() { + return App; + }, + }, components: { GiftedDialog, GiftedPrompts, @@ -367,6 +384,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord { export default class HomeView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; + AppString = AppString; + activeDid = ""; allContacts: Array = []; allMyDids: Array = []; @@ -374,6 +393,7 @@ export default class HomeView extends Vue { feedData: GiveRecordWithContactInfo[] = []; feedPreviousOldestId?: string; feedLastViewedClaimId?: string; + givenName = ""; isAnyFeedFilterOn: boolean; isCreatingIdentifier = false; isFeedFilteredByVisible = false; @@ -397,15 +417,6 @@ export default class HomeView extends Vue { return identity; // may be null } - public async getHeaders(identity: IIdentifier) { - const token = await accessToken(identity); - const headers = { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }; - return headers; - } - async mounted() { try { await accountsDB.open(); @@ -418,6 +429,7 @@ export default class HomeView extends Vue { this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); this.feedLastViewedClaimId = settings?.lastViewedClaimId; + this.givenName = settings?.firstName || ""; this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible; this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby; this.isRegistered = !!settings?.isRegistered; @@ -426,14 +438,7 @@ export default class HomeView extends Vue { this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings); - if (this.allMyDids.length === 0) { - this.isCreatingIdentifier = true; - this.activeDid = await generateSaveAndActivateIdentity(); - this.allMyDids = [this.activeDid]; - this.isCreatingIdentifier = false; - } - - // someone may have have registered after sharing contact info + // someone may have have registered after sharing contact info, so recheck if (!this.isRegistered && this.activeDid) { const identity = await this.getIdentity(this.activeDid); try { @@ -475,6 +480,15 @@ export default class HomeView extends Vue { } } + async generateIdentifier() { + this.isCreatingIdentifier = true; + const account = await registerSaveAndActivatePasskey( + AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""), + ); + this.activeDid = account.did; + this.allMyDids = this.allMyDids.concat(this.activeDid); + this.isCreatingIdentifier = false; + } resultsAreFiltered() { return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; } @@ -483,7 +497,7 @@ export default class HomeView extends Vue { return "Notification" in window; } - public async buildHeaders() { + async buildHeaders() { const headers: HeadersInit = { "Content-Type": "application/json", }; @@ -520,7 +534,7 @@ export default class HomeView extends Vue { * Data loader used by infinite scroller * @param payload is the flag from the InfiniteScroll indicating if it should load **/ - public async loadMoreGives(payload: boolean) { + async loadMoreGives(payload: boolean) { // Since feed now loads projects along the way, it takes longer // and the InfiniteScroll component triggers a load before finished. // One alternative is to totally separate the project link loading. @@ -542,7 +556,7 @@ export default class HomeView extends Vue { } } - public async updateAllFeed() { + async updateAllFeed() { this.isFeedLoading = true; let endOfResults = true; await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) @@ -650,7 +664,7 @@ export default class HomeView extends Vue { * @param beforeId the earliest ID (of previous searches) to search earlier * @return claims in reverse chronological order */ - public async retrieveGives(endorserApiServer: string, beforeId?: string) { + async retrieveGives(endorserApiServer: string, beforeId?: string) { const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const response = await fetch( endorserApiServer + diff --git a/src/views/StartView.vue b/src/views/StartView.vue index 3511af0fe..54587d99f 100644 --- a/src/views/StartView.vue +++ b/src/views/StartView.vue @@ -17,7 +17,7 @@

    - Start Here + Generate an Identity

    @@ -25,33 +25,57 @@

    - Do you want a new identifier of your own? + How do you want to create this identifier?

    -

    - If you haven't used this before, click "Yes" to generate a new - identifier. +

    + A passkey is easy to manage, though it is less + interoperable with other systems for advanced uses. + + +

    -

    - Only click "No" if you have a seed of 12 or 24 words generated - elsewhere. +

    + A new seed allows you full control over the keys, + though you are responsible for backups. + + +

    - - Yes, generate one - -
    + +

    + You can also import an existing seed or derive a new address from an + existing seed. +

    +
    - No, I have a seed + You have a seed Derive new address from existing seed @@ -64,23 +88,38 @@