<template> <QuickNav /> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Breadcrumb --> <div class="mb-8"> <!-- Back --> <div class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()" > <fa icon="chevron-left" class="fa-fw"></fa> </h1> </div> <!-- Heading --> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Test </h1> </div> <div> <h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2> <button @click=" this.$notify( { group: 'alert', type: 'toast', text: 'I\'m a toast. Without a timeout, I\'m stuck.', }, 5000, ) " class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2" > Toast </button> <button @click=" this.$notify( { group: 'alert', type: 'info', title: 'Information Alert', text: 'Just wanted you to know.', }, -1, ) " class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Info </button> <button @click=" this.$notify( { group: 'alert', type: 'success', title: 'Success Alert', text: 'Congratulations!', }, -1, ) " class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2" > Success </button> <button @click=" this.$notify( { group: 'alert', type: 'warning', title: 'Warning Alert', text: 'You might wanna look at this.', }, -1, ) " class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2" > Warning </button> <button @click=" this.$notify( { group: 'alert', type: 'danger', title: 'Danger Alert', text: 'Something terrible has happened!', }, -1, ) " class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2" > Danger </button> <button @click=" this.$notify( { group: 'modal', type: 'notification-permission', }, -1, ) " class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif ON </button> <button @click=" this.$notify( { group: 'modal', type: 'notification-mute', }, -1, ) " class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif MUTE </button> <button @click=" this.$notify( { group: 'modal', type: 'notification-off', }, -1, ) " class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif OFF </button> </div> <div class="mt-8"> <h2 class="text-xl font-bold mb-4">Image Sharing</h2> Populates the "shared-photo" view as if they used "share_target". <input type="file" @change="uploadFile" /> <router-link v-if="showFileNextStep()" :to="{ name: 'shared-photo', query: { fileName }, }" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2" > Go to Shared Page </router-link> </div> <div class="mt-8"> <h2 class="text-xl font-bold mb-4">Passkeys</h2> See console for results. <br /> See existing passkeys in Chrome at: chrome://settings/passkeys <br /> Active DID: {{ activeDid || "nothing, which" }} {{ credIdHex ? "has a passkey ID" : "has no passkey ID" }} <div> Register Passkey <button @click="register()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > Simplewebauthn </button> </div> <div> Create JWT <button @click="createJwtSimplewebauthn()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > Simplewebauthn </button> <button @click="createJwtNavigator()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > Navigator </button> </div> <div v-if="jwt"> Verify New JWT <button @click="verifySimplewebauthn()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > Simplewebauthn </button> <button @click="verifyWebCrypto()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > WebCrypto </button> <button @click="verifyP256()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > p256 - broken </button> </div> <div v-else>Verify New JWT -- requires creation first</div> <button @click="verifyMyJwt()" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2" > Verify Hard-Coded JWT </button> </div> </section> </template> <script lang="ts"> import { Buffer } from "buffer/"; import { Base64URLString } from "@simplewebauthn/types"; import { ref } from "vue"; import { Component, Vue } from "vue-facing-decorator"; import QuickNav from "@/components/QuickNav.vue"; import { AppString, NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import * as vcLib from "@/libs/crypto/vc"; import { PeerSetup, verifyJwtP256, verifyJwtSimplewebauthn, verifyJwtWebCrypto, } from "@/libs/crypto/vc/passkeyDidPeer"; import { AccountKeyInfo, getAccount, registerAndSavePasskey, } from "@/libs/util"; const inputFileNameRef = ref<Blob>(); const TEST_PAYLOAD = { vc: { credentialSubject: { "@context": "https://schema.org", "@type": "GiveAction", description: "pizza", }, }, }; @Component({ components: { QuickNav } }) export default class Help extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; // for file import fileName?: string; // for passkeys credIdHex?: string; activeDid?: string; jwt?: string; peerSetup?: PeerSetup; userName?: string; async mounted() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = (settings?.activeDid as string) || ""; this.userName = settings?.firstName as string; await accountsDB.open(); const account: { identity?: string } | undefined = await accountsDB.accounts .where("did") .equals(this.activeDid) .first(); if (this.activeDid) { if (account) { this.credIdHex = account.passkeyCredIdHex as string; } else { alert("No account found for DID " + this.activeDid); } } } async uploadFile(event: Event) { inputFileNameRef.value = event.target?.["files"][0]; // https://developer.mozilla.org/en-US/docs/Web/API/File // ... plus it has a `type` property from my testing const file = inputFileNameRef.value; if (file != null) { const reader = new FileReader(); reader.onload = async (e) => { const data = e.target?.result as ArrayBuffer; if (data) { const blob = new Blob([new Uint8Array(data)], { type: file.type, }); this.fileName = file.name as string; const temp = await db.temp.get("shared-photo"); if (temp) { await db.temp.update("shared-photo", { blob }); } else { await db.temp.add({ id: "shared-photo", blob }); } } }; reader.readAsArrayBuffer(file as Blob); } } showFileNextStep() { return !!inputFileNameRef.value; } public async register() { const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; if (!this.userName) { this.$notify( { group: "modal", type: "confirm", title: "No Name", text: "You should have a name to attach to this passkey. Would you like to enter your own name first?", onNo: async () => { this.userName = DEFAULT_USERNAME; }, onYes: async () => { this.$router.push({ name: "new-edit-account" }); }, noText: "try again and use " + DEFAULT_USERNAME, }, -1, ); return; } const account = await registerAndSavePasskey( AppString.APP_NAME + " - " + this.userName, ); this.activeDid = account.did; this.credIdHex = account.passkeyCredIdHex; } public async createJwtSimplewebauthn() { const account: AccountKeyInfo | undefined = await getAccount( this.activeDid || "", ); if (!vcLib.isFromPasskey(account)) { alert(`The DID ${this.activeDid} is not passkey-enabled.`); return; } this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtSimplewebauthn( this.activeDid as string, TEST_PAYLOAD, this.credIdHex as string, ); console.log("simple jwt4url", this.jwt); } public async createJwtNavigator() { const account: AccountKeyInfo | undefined = await getAccount( this.activeDid || "", ); if (!vcLib.isFromPasskey(account)) { alert(`The DID ${this.activeDid} is not passkey-enabled.`); return; } this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtNavigator( this.activeDid as string, TEST_PAYLOAD, this.credIdHex as string, ); console.log("lower jwt4url", this.jwt); } public async verifyP256() { const decoded = await verifyJwtP256( this.credIdHex as string, this.activeDid as string, this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup?.challenge as Uint8Array, this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.signature as Base64URLString, ); console.log("decoded", decoded); } public async verifySimplewebauthn() { const decoded = await verifyJwtSimplewebauthn( this.credIdHex as string, this.activeDid as string, this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup?.challenge as Uint8Array, this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.signature as Base64URLString, ); console.log("decoded", decoded); } public async verifyWebCrypto() { const decoded = await verifyJwtWebCrypto( this.credIdHex as string, this.activeDid as string, this.peerSetup?.authenticatorData as ArrayBuffer, this.peerSetup?.challenge as Uint8Array, this.peerSetup?.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.signature as Base64URLString, ); console.log("decoded", decoded); } public async verifyMyJwt() { const did = "did:peer:0zKMFjvUgYrM1hXwDciYHiA9MxXtJPXnRLJvqoMNAKoDLX9pKMWLb3VDsgua1p2zW1xXRsjZSTNsfvMnNyMS7dB4k7NAhFwL3pXBrBXgyYJ9ri"; const jwt = "eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE"; const pieces = jwt.split("."); const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString()); const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64"); const clientJSON = Buffer.from( payload["ClientDataJSONB64URL"], "base64", ).toString(); const clientData = JSON.parse(clientJSON); const challenge = clientData.challenge; const signatureB64URL = pieces[2]; const decoded = await verifyJwtWebCrypto( this.credIdHex as string, did, authData, challenge, payload["ClientDataJSONB64URL"], signatureB64URL, ); console.log("decoded", decoded); } } </script>