passkey test #116
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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, "=");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user