Browse Source

save passkey DID in accounts, consolidate more data

pull/116/head
Trent Larson 5 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; 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; publicKeyHex: string;
}; };

45
src/libs/didPeer.ts

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

82
src/views/TestView.vue

@ -172,6 +172,10 @@
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2> <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> <div>
Register Register
<button <button
@ -222,13 +226,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Buffer } from "buffer/";
import { Base64URLString } from "@simplewebauthn/types"; import { Base64URLString } from "@simplewebauthn/types";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { generateRandomBytes } from "@/libs/crypto";
import { import {
createPeerDid, createPeerDid,
PeerSetup, PeerSetup,
@ -238,6 +242,7 @@ import {
verifyJwtWebCrypto, verifyJwtWebCrypto,
} from "@/libs/didPeer"; } from "@/libs/didPeer";
import { JWTPayload } from "did-jwt"; import { JWTPayload } from "did-jwt";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@ -247,12 +252,30 @@ export default class Help extends Vue {
fileName?: string; fileName?: string;
// for passkeys // for passkeys
credId?: Base64URLString; credIdHex?: string;
did?: string; activeDid?: string;
jwt?: string; jwt?: string;
peerSetup?: PeerSetup; peerSetup?: PeerSetup;
rawId?: Uint8Array; 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) { async uploadFile(event: Event) {
inputFileNameRef.value = event.target.files[0]; inputFileNameRef.value = event.target.files[0];
@ -285,19 +308,26 @@ export default class Help extends Vue {
} }
public async register() { public async register() {
this.userId = generateRandomBytes(16).buffer; const cred = await registerCredential(this.userName);
const publicKeyBytes = cred.publicKeyBytes;
const cred = await registerCredential(this.userId as Uint8Array); this.activeDid = createPeerDid(publicKeyBytes as Uint8Array);
console.log("public key", cred); this.credIdHex = cred.credIdHex as string;
this.publicKeyBytes = cred.publicKeyBytes;
this.did = createPeerDid(this.publicKeyBytes as Uint8Array);
this.credId = cred.credId as string;
this.rawId = cred.rawId as Uint8Array; 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() { public async createJwtSimplewebauthn() {
console.log("generated peer did", this.did);
const payload = { const payload = {
"@context": "https://schema.org", "@context": "https://schema.org",
type: "GiveAction", type: "GiveAction",
@ -306,20 +336,19 @@ export default class Help extends Vue {
// from createJWT in did-jwt/src/JWT.ts // from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = { const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000), 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.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtSimplewebauthn( this.jwt = await this.peerSetup.createJwtSimplewebauthn(
fullPayload, fullPayload,
this.credId as string, this.credIdHex as string,
); );
console.log("simple jwt4url", this.jwt); console.log("simple jwt4url", this.jwt);
} }
public async createJwtNavigator() { public async createJwtNavigator() {
console.log("generated peer did", this.did); console.log("generated peer did", this.activeDid);
const payload = { const payload = {
"@context": "https://schema.org", "@context": "https://schema.org",
@ -329,23 +358,22 @@ export default class Help extends Vue {
// from createJWT in did-jwt/src/JWT.ts // from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = { const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000), 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.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator( this.jwt = await this.peerSetup.createJwtNavigator(
fullPayload, fullPayload,
this.credId as string, this.credIdHex as string,
); );
console.log("lower jwt4url", this.jwt); console.log("lower jwt4url", this.jwt);
} }
public async verifyP256() { public async verifyP256() {
const decoded = await verifyJwtP256( const decoded = await verifyJwtP256(
this.credId as Base64URLString, this.credIdHex as Base64URLString,
this.rawId as Uint8Array, this.rawId as Uint8Array,
this.did as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
@ -356,9 +384,9 @@ export default class Help extends Vue {
public async verifySimplewebauthn() { public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn( const decoded = await verifyJwtSimplewebauthn(
this.credId as Base64URLString, this.credIdHex as Base64URLString,
this.rawId as Uint8Array, this.rawId as Uint8Array,
this.did as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
@ -369,9 +397,9 @@ export default class Help extends Vue {
public async verifyWebCrypto() { public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credId as Base64URLString, this.credIdHex as Base64URLString,
this.rawId as Uint8Array, this.rawId as Uint8Array,
this.did as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,

Loading…
Cancel
Save