save passkey DID in accounts, consolidate more data
This commit is contained in:
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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, "=");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user