Browse Source

misc syntactic & type-checking clean-up

Trent Larson 5 months ago
parent
commit
63d0f3c748
  1. 41
      src/libs/crypto/passkeyHelpers.ts
  2. 16
      src/libs/didPeer.ts
  3. 4
      src/views/ImportDerivedAccountView.vue
  4. 70
      src/views/TestView.vue

41
src/libs/crypto/passkeyHelpers.ts

@ -55,8 +55,8 @@ export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
} }
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts // from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
let webCrypto: unknown = undefined; let webCrypto: { subtle: SubtleCrypto } | undefined = undefined;
export function getWebCrypto() { export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
/** /**
* Hello there! If you came here wondering why this method is asynchronous when use of * Hello there! If you came here wondering why this method is asynchronous when use of
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this * `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
@ -67,22 +67,25 @@ export function getWebCrypto() {
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense * TODO: If it's after February 2025 when you read this then consider whether it still makes sense
* to keep this method asynchronous. * to keep this method asynchronous.
*/ */
const toResolve = new Promise((resolve, reject) => { const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise(
if (webCrypto) { (resolve, reject) => {
return resolve(webCrypto); if (webCrypto) {
} return resolve(webCrypto);
/** }
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times /**
* support (and Node v20+) * Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
*/ * support (and Node v20+)
const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto(); */
if (_globalThisCrypto) { const _globalThisCrypto =
webCrypto = _globalThisCrypto; _getWebCryptoInternals.stubThisGlobalThisCrypto();
return resolve(webCrypto); if (_globalThisCrypto) {
} webCrypto = _globalThisCrypto;
// We tried to access it both in Node and globally, so bail out return resolve(webCrypto);
return reject(new MissingWebCrypto()); }
}); // We tried to access it both in Node and globally, so bail out
return reject(new MissingWebCrypto());
},
);
return toResolve; return toResolve;
} }
export class MissingWebCrypto extends Error { export class MissingWebCrypto extends Error {
@ -96,7 +99,7 @@ export class MissingWebCrypto extends Error {
export const _getWebCryptoInternals = { export const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto, stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file // Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: unknown) => { setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
webCrypto = newCrypto; webCrypto = newCrypto;
}, },
}; };

16
src/libs/didPeer.ts

@ -74,7 +74,7 @@ export async function registerCredential(passkeyName?: string) {
const credIdBase64Url = verification.registrationInfo?.credentialID as string; const credIdBase64Url = verification.registrationInfo?.credentialID as string;
if (attResp.rawId !== credIdBase64Url) { if (attResp.rawId !== credIdBase64Url) {
console.log("Warning! The raw ID does not match the credential ID.") console.log("Warning! The raw ID does not match the credential ID.");
} }
const credIdHex = Buffer.from( const credIdHex = Buffer.from(
base64URLStringToArrayBuffer(credIdBase64Url), base64URLStringToArrayBuffer(credIdBase64Url),
@ -237,8 +237,9 @@ export class PeerSetup {
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/=+$/, ""); .replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature) const origSignature = Buffer.from(credential?.response.signature).toString(
.toString("base64") "base64",
);
this.signature = origSignature this.signature = origSignature
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_") .replace(/\//g, "_")
@ -423,6 +424,7 @@ export async function verifyJwtWebCrypto(
return verified; return verified;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
if (!did.startsWith("did:peer:0z")) { if (!did.startsWith("did:peer:0z")) {
throw new Error( throw new Error(
@ -463,12 +465,15 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
} }
// convert COSE public key to PEM format // convert COSE public key to PEM format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function COSEtoPEM(cose: Buffer) { function COSEtoPEM(cose: Buffer) {
// const alg = cose.get(3); // Algorithm // const alg = cose.get(3); // Algorithm
const x = cose[-2]; // x-coordinate const x = cose[-2]; // x-coordinate
const y = cose[-3]; // y-coordinate const y = cose[-3]; // y-coordinate
// Ensure the coordinates are in the correct format // Ensure the coordinates are in the correct format
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error because it complains about the type of x and y
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]); const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
// Convert to PEM format // Convert to PEM format
@ -479,6 +484,7 @@ ${pubKeyBuffer.toString("base64")}
return pem; return pem;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecode(input: string) { function base64urlDecode(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/"); input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4); const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
@ -490,13 +496,14 @@ function base64urlDecode(input: string) {
return bytes.buffer; return bytes.buffer;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncode(buffer: ArrayBuffer) { function base64urlEncode(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer)); const str = String.fromCharCode(...new Uint8Array(buffer));
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
} }
// from @simplewebauthn/browser // from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer) { function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
let str = ""; let str = "";
for (const charCode of bytes) { for (const charCode of bytes) {
@ -545,6 +552,7 @@ function cborToKeys(publicKeyBytes: Uint8Array) {
return { publicKeyJwk, publicKeyBuffer }; return { publicKeyJwk, publicKeyBuffer };
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function pemToCryptoKey(pem: string) { async function pemToCryptoKey(pem: string) {
const binaryDerString = atob( const binaryDerString = atob(
pem pem

4
src/views/ImportDerivedAccountView.vue

@ -17,7 +17,7 @@
<div> <div>
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Will increment the maximum derivation path from the existing seed. Will increment the maximum known derivation path from the existing seed.
</p> </p>
<p v-if="didArrays.length > 1"> <p v-if="didArrays.length > 1">
@ -75,7 +75,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
nextDerivationPath, nextDerivationPath,
} from "../libs/crypto"; } from "@/libs/crypto";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";

70
src/views/TestView.vue

@ -173,59 +173,65 @@
<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. See console for results.
<br/> <br />
See existing passkeys in Chrome at: chrome://settings/passkeys
<br />
Active DID: {{ activeDid }} Active DID: {{ activeDid }}
{{ credIdHex ? "has passkey ID" : "has no passkey ID" }} {{ credIdHex ? "has passkey ID" : "has no passkey ID" }}
<div> <div>
Register Register Passkey
<button <button
@click="register()" @click="register()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
</div> </div>
<div> <div>
Create Create JWT
<button <button
@click="createJwtSimplewebauthn()" @click="createJwtSimplewebauthn()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="createJwtNavigator()" @click="createJwtNavigator()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Navigator Navigator
</button> </button>
</div> </div>
<div v-if="jwt"> <div v-if="jwt">
Verify Verify New JWT
<button <button
@click="verifySimplewebauthn()" @click="verifySimplewebauthn()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="verifyWebCrypto()" @click="verifyWebCrypto()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
WebCrypto WebCrypto
</button> </button>
<button <button
@click="verifyP256()" @click="verifyP256()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
p256 - broken p256 - broken
</button> </button>
</div> </div>
<div v-else>Verify New JWT -- requires creation first</div>
<button <button
@click="verifyMyJwt()" @click="verifyMyJwt()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Verify Mine Verify Hard-Coded JWT
</button> </button>
</div> </div>
</section> </section>
@ -335,7 +341,7 @@ export default class Help extends Vue {
did: this.activeDid, did: this.activeDid,
passkeyCredIdHex: this.credIdHex, passkeyCredIdHex: this.credIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"), publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
});`` });
} }
public async createJwtSimplewebauthn() { public async createJwtSimplewebauthn() {
@ -360,44 +366,46 @@ export default class Help extends Vue {
public async verifyP256() { public async verifyP256() {
const decoded = await verifyJwtP256( const decoded = await verifyJwtP256(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid 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,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifySimplewebauthn() { public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn( const decoded = await verifyJwtSimplewebauthn(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid 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,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyWebCrypto() { public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid 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,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyMyJwt() { public async verifyMyJwt() {
const did =
"did:peer:0zKMFjvUgYrM1hXwDciYHiA9MxXtJPXnRLJvqoMNAKoDLX9pKMWLb3VDsgua1p2zW1xXRsjZSTNsfvMnNyMS7dB4k7NAhFwL3pXBrBXgyYJ9ri";
const jwt = const jwt =
"eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE"; "eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE";
const pieces = jwt.split("."); const pieces = jwt.split(".");
console.log("pieces", typeof pieces[1], pieces);
const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString()); const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString());
const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64"); const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64");
const clientJSON = Buffer.from( const clientJSON = Buffer.from(
@ -408,8 +416,8 @@ export default class Help extends Vue {
const challenge = clientData.challenge; const challenge = clientData.challenge;
const signatureB64URL = pieces[2]; const signatureB64URL = pieces[2];
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid as string, did,
authData, authData,
challenge, challenge,
payload["ClientDataJSONB64URL"], payload["ClientDataJSONB64URL"],

Loading…
Cancel
Save