diff --git a/src/db/tables/accounts.ts b/src/db/tables/accounts.ts index 63f4c6a..a793f9c 100644 --- a/src/db/tables/accounts.ts +++ b/src/db/tables/accounts.ts @@ -3,41 +3,46 @@ */ export type Account = { /** - * Auto-generated ID by Dexie. + * Auto-generated ID by Dexie */ id?: number; /** - * The date the account was created. + * The date the account was created */ dateCreated: string; /** - * The derivation path for the account. + * The derivation path for the account, if this is from a mnemonic */ - derivationPath: string; + derivationPath?: string; /** - * Decentralized Identifier (DID) for the account. + * Decentralized Identifier (DID) for the account */ did: string; /** - * Stringified JSON containing underlying key material. - * Based on the IIdentifier type from Veramo. + * Stringified JSON containing underlying key material, if generated from a mnemonic + * Based on the IIdentifier type from Veramo * @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts} */ - identity: string; + identity?: string; /** - * The public key in hexadecimal format. + * The mnemonic phrase for the account, if this is from a mnemonic */ - publicKeyHex: string; + mnemonic?: string; /** - * The mnemonic passphrase for the account. + * The Webauthn credential ID, if this is from a passkey */ - mnemonic: string; + passkeyCredId?: string; + + /** + * The public key in hexadecimal format. + */ + publicKeyHex: string; }; /** diff --git a/src/libs/didPeer.ts b/src/libs/didPeer.ts index b6eb80f..dacd8e0 100644 --- a/src/libs/didPeer.ts +++ b/src/libs/didPeer.ts @@ -1,10 +1,9 @@ import asn1 from "asn1-ber"; import { Buffer } from "buffer/"; import { decode as cborDecode } from "cbor-x"; -import { JWTPayload } from "did-jwt"; +import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt"; import { DIDResolutionResult } from "did-resolver"; import { sha256 } from "ethereum-cryptography/sha256.js"; -import { bytesToMultibase } from "@veramo/utils"; import { startAuthentication, startRegistration, @@ -24,6 +23,7 @@ import { import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; +const PEER_DID_PREFIX = "did:peer:0"; export interface JWK { kty: string; crv: string; @@ -97,14 +97,16 @@ export function createPeerDid(publicKeyBytes: Uint8Array) { "base58btc", "p256-pub", ); - return "did:peer:0" + methodSpecificId; + return PEER_DID_PREFIX + methodSpecificId; +} + +function peerDidToPublicKeyBytes(did: string) { + return multibaseToBytes(did.substring(PEER_DID_PREFIX.length)); } export class PeerSetup { public authenticatorData?: ArrayBuffer; - public authenticatorDataBase64Url?: Base64URLString; public challenge?: Uint8Array; - public clientDataJsonDecoded?: object; public clientDataJsonBase64Url?: Base64URLString; public signature?: Base64URLString; @@ -124,17 +126,12 @@ export class PeerSetup { const clientAuth = await startAuthentication(options); // console.log("simple credential get", clientAuth); - this.authenticatorDataBase64Url = clientAuth.response.authenticatorData; + const authenticatorDataBase64Url = clientAuth.response.authenticatorData; this.authenticatorData = Buffer.from( clientAuth.response.authenticatorData, "base64", ).buffer; this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; - this.clientDataJsonDecoded = JSON.parse( - Buffer.from(clientAuth.response.clientDataJSON, "base64").toString( - "utf-8", - ), - ); // console.log("simple authenticatorData for signing", this.authenticatorData); this.signature = clientAuth.response.signature; @@ -147,7 +144,7 @@ export class PeerSetup { .replace(/=+$/, ""); const dataInJwt = { - AuthenticationData: this.authenticatorDataBase64Url, + AuthenticationData: authenticatorDataBase64Url, ClientDataJSON: this.clientDataJsonBase64Url, }; const dataInJwtString = JSON.stringify(dataInJwt); @@ -175,20 +172,14 @@ export class PeerSetup { const credential = await navigator.credentials.get(options); // console.log("nav credential get", credential); - this.authenticatorDataBase64Url = bufferToBase64URLString( - credential?.response.authenticatorData, + this.authenticatorData = credential?.response.authenticatorData; + const authenticatorDataBase64Url = bufferToBase64URLString( + this.authenticatorData, ); - this.authenticatorData = Buffer.from( - this.authenticatorDataBase64Url as Base64URLString, - "base64", - ).buffer; this.clientDataJsonBase64Url = bufferToBase64URLString( credential?.response.clientDataJSON, ); - this.clientDataJsonDecoded = JSON.parse( - new TextDecoder("utf-8").decode(credential?.response.clientDataJSON), - ); // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; @@ -199,7 +190,7 @@ export class PeerSetup { .replace(/=+$/, ""); const dataInJwt = { - AuthenticationData: this.authenticatorDataBase64Url, + AuthenticationData: authenticatorDataBase64Url, ClientDataJSON: this.clientDataJsonBase64Url, }; const dataInJwtString = JSON.stringify(dataInJwt); @@ -276,20 +267,51 @@ export class PeerSetup { // } } +// I'd love to use this but it doesn't verify. +// Pequires: +// npm install @noble/curves +// ... and this import: +// import { p256 } from "@noble/curves/p256"; +export async function verifyJwtP256( + credId: Base64URLString, + rawId: Uint8Array, + did: string, + authenticatorData: ArrayBuffer, + challenge: Uint8Array, + clientDataJsonBase64Url: Base64URLString, + signature: Base64URLString, +) { + const authDataFromBase = Buffer.from(authenticatorData); + const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); + const sigBuffer = Buffer.from(signature, "base64"); + const finalSigBuffer = unwrapEC2Signature(sigBuffer); + const publicKeyBytes = peerDidToPublicKeyBytes(did); + + // Hash the client data + const hash = sha256(clientDataFromBase); + + // Construct the preimage + const preimage = Buffer.concat([authDataFromBase, hash]); + + const isValid = p256.verify( + finalSigBuffer, + new Uint8Array(preimage), + publicKeyBytes, + ); + return isValid; +} + export async function verifyJwtSimplewebauthn( - jwt: string, credId: Base64URLString, rawId: Uint8Array, + did: string, authenticatorData: ArrayBuffer, - authenticatorDataBase64Url: Base64URLString, challenge: Uint8Array, - clientDataJSON: object, clientDataJsonBase64Url: Base64URLString, - publicKeyBytes: Uint8Array, - publicKeyJwk: JWK, signature: Base64URLString, ) { const authData = arrayToBase64Url(Buffer.from(authenticatorData)); + const publicKeyBytes = peerDidToPublicKeyBytes(did); const authOpts: VerifyAuthenticationResponseOpts = { authenticator: { credentialID: credId, @@ -306,9 +328,7 @@ export async function verifyJwtSimplewebauthn( rawId: arrayToBase64Url(rawId), response: { authenticatorData: authData, - clientDataJSON: arrayToBase64Url( - Buffer.from(JSON.stringify(clientDataJSON)), - ), + clientDataJSON: clientDataJsonBase64Url, signature: signature, }, type: "public-key", @@ -318,57 +338,16 @@ export async function verifyJwtSimplewebauthn( return verification.verified; } -// I'd love to use this but it doesn't verify. -// Pequires: -// npm install @noble/curves -// ... and this import: -// import { p256 } from "@noble/curves/p256"; -export async function verifyJwtP256( - jwt: string, - credId: Base64URLString, - rawId: Uint8Array, - authenticatorData: ArrayBuffer, - authenticatorDataBase64Url: Base64URLString, - challenge: Uint8Array, - clientDataJSON: object, - clientDataJsonBase64Url: Base64URLString, - publicKeyBytes: Uint8Array, - publicKeyJwk: JWK, - signature: Base64URLString, -) { - const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64"); - const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); - const sigBuffer = Buffer.from(signature, "base64"); - const finalSigBuffer = unwrapEC2Signature(sigBuffer); - - // Hash the client data - const hash = sha256(clientDataFromBase); - - // Construct the preimage - const preimage = Buffer.concat([authDataFromBase, hash]); - - const isValid = p256.verify( - finalSigBuffer, - new Uint8Array(preimage), - publicKeyBytes, - ); - return isValid; -} - export async function verifyJwtWebCrypto( - jwt: string, credId: Base64URLString, rawId: Uint8Array, + did: string, authenticatorData: ArrayBuffer, - authenticatorDataBase64Url: Base64URLString, challenge: Uint8Array, - clientDataJSON: object, clientDataJsonBase64Url: Base64URLString, - publicKeyBytes: Uint8Array, - publicKeyJwk: JWK, signature: Base64URLString, ) { - const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64"); + const authDataFromBase = Buffer.from(authenticatorData); const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); const sigBuffer = Buffer.from(signature, "base64"); const finalSigBuffer = unwrapEC2Signature(sigBuffer); @@ -384,6 +363,8 @@ export async function verifyJwtWebCrypto( name: "ECDSA", hash: { name: "SHA-256" }, }; + const publicKeyBytes = peerDidToPublicKeyBytes(did); + const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; const keyAlgorithm = { name: "ECDSA", namedCurve: publicKeyJwk.crv, @@ -479,19 +460,19 @@ function base64urlEncode(buffer: ArrayBuffer) { // from @simplewebauthn/browser function bufferToBase64URLString(buffer) { const bytes = new Uint8Array(buffer); - let str = ''; + let str = ""; for (const charCode of bytes) { str += String.fromCharCode(charCode); } const base64String = btoa(str); - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } // from @simplewebauthn/browser function base64URLStringToBuffer(base64URLString) { - const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); + const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/"); const padLength = (4 - (base64.length % 4)) % 4; - const padded = base64.padEnd(base64.length + padLength, '='); + const padded = base64.padEnd(base64.length + padLength, "="); const binary = atob(padded); const buffer = new ArrayBuffer(binary.length); const bytes = new Uint8Array(buffer); diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 10e3a3e..a5c6ac6 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -231,9 +231,9 @@ import { db } from "@/db/index"; import { generateRandomBytes } from "@/libs/crypto"; import { createPeerDid, - JWK, PeerSetup, - registerCredential, verifyJwtP256, + registerCredential, + verifyJwtP256, verifyJwtSimplewebauthn, verifyJwtWebCrypto, } from "@/libs/didPeer"; @@ -248,10 +248,9 @@ export default class Help extends Vue { // for passkeys credId?: Base64URLString; + did?: string; jwt?: string; peerSetup?: PeerSetup; - publicKeyJwk?: JWK; - publicKeyBytes?: Uint8Array; rawId?: Uint8Array; userId?: ArrayBuffer; @@ -290,15 +289,14 @@ export default class Help extends Vue { const cred = await registerCredential(this.userId as Uint8Array); console.log("public key", cred); - this.publicKeyJwk = cred.publicKeyJwk; this.publicKeyBytes = cred.publicKeyBytes; + this.did = createPeerDid(this.publicKeyBytes as Uint8Array); this.credId = cred.credId as string; this.rawId = cred.rawId as Uint8Array; } public async createJwtSimplewebauthn() { - const did = createPeerDid(this.publicKeyBytes as Uint8Array); - console.log("generated peer did", did); + console.log("generated peer did", this.did); const payload = { "@context": "https://schema.org", @@ -310,7 +308,7 @@ export default class Help extends Vue { iat: Math.floor(Date.now() / 1000), exp: undefined, }; - const fullPayload = { ...timestamps, ...payload, did }; + const fullPayload = { ...timestamps, ...payload, iss: this.did }; this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtSimplewebauthn( @@ -321,8 +319,7 @@ export default class Help extends Vue { } public async createJwtNavigator() { - const did = createPeerDid(this.publicKeyBytes as Uint8Array); - console.log("generated peer did", did); + console.log("generated peer did", this.did); const payload = { "@context": "https://schema.org", @@ -334,7 +331,7 @@ export default class Help extends Vue { iat: Math.floor(Date.now() / 1000), exp: undefined, }; - const fullPayload = { ...timestamps, ...payload, did }; + const fullPayload = { ...timestamps, ...payload, iss: this.did }; this.peerSetup = new PeerSetup(); this.jwt = await this.peerSetup.createJwtNavigator( @@ -346,16 +343,12 @@ export default class Help extends Vue { public async verifyP256() { const decoded = await verifyJwtP256( - this.jwt as string, this.credId as Base64URLString, this.rawId as Uint8Array, + this.did as string, this.peerSetup.authenticatorData as ArrayBuffer, - this.peerSetup.authenticatorDataBase64Url as Base64URLString, this.peerSetup.challenge as Uint8Array, - this.peerSetup.clientDataJsonDecoded, this.peerSetup.clientDataJsonBase64Url as Base64URLString, - this.publicKeyBytes as Uint8Array, - this.publicKeyJwk as JWK, this.peerSetup.signature as Base64URLString, ); console.log("decoded", decoded); @@ -363,16 +356,12 @@ export default class Help extends Vue { public async verifySimplewebauthn() { const decoded = await verifyJwtSimplewebauthn( - this.jwt as string, this.credId as Base64URLString, this.rawId as Uint8Array, + this.did as string, this.peerSetup.authenticatorData as ArrayBuffer, - this.peerSetup.authenticatorDataBase64Url as Base64URLString, this.peerSetup.challenge as Uint8Array, - this.peerSetup.clientDataJsonDecoded, this.peerSetup.clientDataJsonBase64Url as Base64URLString, - this.publicKeyBytes as Uint8Array, - this.publicKeyJwk as JWK, this.peerSetup.signature as Base64URLString, ); console.log("decoded", decoded); @@ -380,16 +369,12 @@ export default class Help extends Vue { public async verifyWebCrypto() { const decoded = await verifyJwtWebCrypto( - this.jwt as string, this.credId as Base64URLString, this.rawId as Uint8Array, + this.did as string, this.peerSetup.authenticatorData as ArrayBuffer, - this.peerSetup.authenticatorDataBase64Url as Base64URLString, this.peerSetup.challenge as Uint8Array, - this.peerSetup.clientDataJsonDecoded, this.peerSetup.clientDataJsonBase64Url as Base64URLString, - this.publicKeyBytes as Uint8Array, - this.publicKeyJwk as JWK, this.peerSetup.signature as Base64URLString, ); console.log("decoded", decoded);