Browse Source

save passkey DID in accounts, consolidate more data

pull/116/head
Trent Larson 3 months ago
parent
commit
f8ade5289b
  1. 6
      src/db/tables/accounts.ts
  2. 45
      src/libs/didPeer.ts
  3. 82
      src/views/TestView.vue

6
src/db/tables/accounts.ts

@ -35,12 +35,12 @@ export type Account = {
mnemonic?: string;
/**
* The Webauthn credential ID, if this is from a passkey
* The Webauthn credential ID in hex, if this is from a passkey
*/
passkeyCredId?: string;
passkeyCredIdHex?: string;
/**
* The public key in hexadecimal format.
* The public key in hexadecimal format
*/
publicKeyHex: string;
};

45
src/libs/didPeer.ts

@ -43,12 +43,12 @@ function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export async function registerCredential(userId: Uint8Array) {
export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({
rpName: "Time Safari",
rpID: window.location.hostname,
userName: "Current-User",
userName: passkeyName || "Time Safari User",
// Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX)
attestationType: "none",
@ -75,13 +75,17 @@ export async function registerCredential(userId: Uint8Array) {
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
const credIdBase64Url = verification.registrationInfo?.credentialID as string;
const credIdHex = Buffer.from(
base64URLStringToArrayBuffer(credIdBase64Url),
).toString("hex");
const { publicKeyJwk } = cborToKeys(
verification.registrationInfo?.credentialPublicKey as Uint8Array,
);
return {
authData: verification.registrationInfo?.attestationObject,
credId: verification.registrationInfo?.credentialID as string,
credIdHex: credIdHex,
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")),
publicKeyJwk: publicKeyJwk,
publicKeyBytes: verification.registrationInfo
@ -110,16 +114,17 @@ export class PeerSetup {
public clientDataJsonBase64Url?: Base64URLString;
public signature?: Base64URLString;
public async createJwtSimplewebauthn(
fullPayload: object,
credentialId: string,
) {
public async createJwtSimplewebauthn(fullPayload: object, credIdHex: string) {
const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer,
);
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
// const payloadHash: Uint8Array = sha256(this.challenge);
const options: PublicKeyCredentialRequestOptionsJSON =
await generateAuthenticationOptions({
challenge: this.challenge,
rpID: window.location.hostname,
allowCredentials: [{ id: credentialId }],
});
// console.log("simple authentication options", options);
@ -155,14 +160,21 @@ export class PeerSetup {
return headerBase64 + "." + payloadBase64 + "." + signature;
}
public async createJwtNavigator(fullPayload: object, credentialId: string) {
public async createJwtNavigator(fullPayload: object, credIdHex: string) {
const dataToSignString = JSON.stringify(fullPayload);
const dataToSignBuffer = Buffer.from(dataToSignString);
const credentialId = Buffer.from(credIdHex, "hex");
// console.log("lower credentialId", credentialId);
this.challenge = new Uint8Array(dataToSignBuffer);
const options = {
publicKey: {
allowCredentials: [
{
id: credentialId,
type: "public-key",
},
],
challenge: this.challenge.buffer,
rpID: window.location.hostname,
userVerification: "preferred",
@ -173,11 +185,11 @@ export class PeerSetup {
// console.log("nav credential get", credential);
this.authenticatorData = credential?.response.authenticatorData;
const authenticatorDataBase64Url = bufferToBase64URLString(
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
this.authenticatorData,
);
this.clientDataJsonBase64Url = bufferToBase64URLString(
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
credential?.response.clientDataJSON,
);
@ -268,12 +280,12 @@ export class PeerSetup {
}
// I'd love to use this but it doesn't verify.
// Pequires:
// Requires:
// npm install @noble/curves
// ... and this import:
// import { p256 } from "@noble/curves/p256";
export async function verifyJwtP256(
credId: Base64URLString,
credIdHex: string,
rawId: Uint8Array,
did: string,
authenticatorData: ArrayBuffer,
@ -302,7 +314,7 @@ export async function verifyJwtP256(
}
export async function verifyJwtSimplewebauthn(
credId: Base64URLString,
credIdHex: string,
rawId: Uint8Array,
did: string,
authenticatorData: ArrayBuffer,
@ -312,6 +324,9 @@ export async function verifyJwtSimplewebauthn(
) {
const authData = arrayToBase64Url(Buffer.from(authenticatorData));
const publicKeyBytes = peerDidToPublicKeyBytes(did);
const credId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer,
);
const authOpts: VerifyAuthenticationResponseOpts = {
authenticator: {
credentialID: credId,
@ -458,7 +473,7 @@ function base64urlEncode(buffer: ArrayBuffer) {
}
// from @simplewebauthn/browser
function bufferToBase64URLString(buffer) {
function arrayBufferToBase64URLString(buffer) {
const bytes = new Uint8Array(buffer);
let str = "";
for (const charCode of bytes) {
@ -469,7 +484,7 @@ function bufferToBase64URLString(buffer) {
}
// from @simplewebauthn/browser
function base64URLStringToBuffer(base64URLString) {
function base64URLStringToArrayBuffer(base64URLString: string) {
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
const padLength = (4 - (base64.length % 4)) % 4;
const padded = base64.padEnd(base64.length + padLength, "=");

82
src/views/TestView.vue

@ -172,6 +172,10 @@
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
<br/>
Active DID: {{ activeDid }}
{{ credIdHex ? "has passkey ID" : "has no passkey ID" }}
<div>
Register
<button
@ -222,13 +226,13 @@
</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 { db } from "@/db/index";
import { generateRandomBytes } from "@/libs/crypto";
import { accountsDB, db } from "@/db/index";
import {
createPeerDid,
PeerSetup,
@ -238,6 +242,7 @@ import {
verifyJwtWebCrypto,
} from "@/libs/didPeer";
import { JWTPayload } from "did-jwt";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
const inputFileNameRef = ref<Blob>();
@ -247,12 +252,30 @@ export default class Help extends Vue {
fileName?: string;
// for passkeys
credId?: Base64URLString;
did?: string;
credIdHex?: string;
activeDid?: string;
jwt?: string;
peerSetup?: PeerSetup;
rawId?: Uint8Array;
userId?: ArrayBuffer;
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];
@ -285,19 +308,26 @@ export default class Help extends Vue {
}
public async register() {
this.userId = generateRandomBytes(16).buffer;
const cred = await registerCredential(this.userId as Uint8Array);
console.log("public key", cred);
this.publicKeyBytes = cred.publicKeyBytes;
this.did = createPeerDid(this.publicKeyBytes as Uint8Array);
this.credId = cred.credId as string;
const cred = await registerCredential(this.userName);
const publicKeyBytes = cred.publicKeyBytes;
this.activeDid = createPeerDid(publicKeyBytes as Uint8Array);
this.credIdHex = cred.credIdHex as string;
this.rawId = cred.rawId as Uint8Array;
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
did: this.activeDid,
passkeyCredIdHex: this.credIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
});
// await db.settings.update(MASTER_SETTINGS_KEY, {
// activeDid: this.did,
// });
}
public async createJwtSimplewebauthn() {
console.log("generated peer did", this.did);
const payload = {
"@context": "https://schema.org",
type: "GiveAction",
@ -306,20 +336,19 @@ export default class Help extends Vue {
// from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000),
exp: undefined,
};
const fullPayload = { ...timestamps, ...payload, iss: this.did };
const fullPayload = { ...timestamps, ...payload, iss: this.activeDid };
this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtSimplewebauthn(
fullPayload,
this.credId as string,
this.credIdHex as string,
);
console.log("simple jwt4url", this.jwt);
}
public async createJwtNavigator() {
console.log("generated peer did", this.did);
console.log("generated peer did", this.activeDid);
const payload = {
"@context": "https://schema.org",
@ -329,23 +358,22 @@ export default class Help extends Vue {
// from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000),
exp: undefined,
};
const fullPayload = { ...timestamps, ...payload, iss: this.did };
const fullPayload = { ...timestamps, ...payload, iss: this.activeDid };
this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator(
fullPayload,
this.credId as string,
this.credIdHex as string,
);
console.log("lower jwt4url", this.jwt);
}
public async verifyP256() {
const decoded = await verifyJwtP256(
this.credId as Base64URLString,
this.credIdHex as Base64URLString,
this.rawId as Uint8Array,
this.did as string,
this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString,
@ -356,9 +384,9 @@ export default class Help extends Vue {
public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn(
this.credId as Base64URLString,
this.credIdHex as Base64URLString,
this.rawId as Uint8Array,
this.did as string,
this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString,
@ -369,9 +397,9 @@ export default class Help extends Vue {
public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto(
this.credId as Base64URLString,
this.credIdHex as Base64URLString,
this.rawId as Uint8Array,
this.did as string,
this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString,

Loading…
Cancel
Save