forked from trent_larson/crowd-funder-for-time-pwa
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import { Buffer } from "buffer/";
|
|
import { decode as cborDecode } from "cbor-x";
|
|
import { createJWS, JWTPayload, verifyJWT } from "did-jwt";
|
|
import { getResolver } from "@veramo/did-provider-peer";
|
|
|
|
import { generateRandomBytes } from "@/libs/crypto";
|
|
|
|
export interface JWK {
|
|
kty: string;
|
|
crv: string;
|
|
x: string;
|
|
y: string;
|
|
}
|
|
export interface PublicKeyCredential {
|
|
rawId: Uint8Array;
|
|
jwt: JWK;
|
|
}
|
|
|
|
export async function registerCredential(
|
|
userId: Uint8Array,
|
|
challenge: Uint8Array,
|
|
) {
|
|
const publicKeyOptions: PublicKeyCredentialCreationOptions = {
|
|
challenge: challenge,
|
|
rp: {
|
|
name: "Time Safari",
|
|
id: window.location.hostname,
|
|
},
|
|
user: {
|
|
id: userId,
|
|
name: "current-user",
|
|
displayName: "Current User",
|
|
},
|
|
pubKeyCredParams: [
|
|
{
|
|
type: "public-key",
|
|
alg: -7, // ES256 algorithm
|
|
},
|
|
],
|
|
authenticatorSelection: {
|
|
authenticatorAttachment: "platform",
|
|
userVerification: "preferred",
|
|
},
|
|
timeout: 60000,
|
|
attestation: "direct",
|
|
};
|
|
|
|
const credential = await navigator.credentials.create({
|
|
publicKey: publicKeyOptions,
|
|
});
|
|
console.log("credential", credential);
|
|
const attestationResponse = credential?.response;
|
|
|
|
// Parse the attestation response to get the public key
|
|
const clientDataJSON = attestationResponse.clientDataJSON;
|
|
console.log("clientDataJSON raw", clientDataJSON);
|
|
console.log(
|
|
"clientDataJSON dec",
|
|
new TextDecoder("utf-8").decode(clientDataJSON),
|
|
);
|
|
const attestationObject = cborDecode(
|
|
new Uint8Array(attestationResponse.attestationObject),
|
|
);
|
|
console.log("attestationObject", attestationObject);
|
|
|
|
const authData = new Uint8Array(attestationObject.authData);
|
|
const publicKey = extractPublicKey(authData);
|
|
|
|
return { rawId: credential?.rawId, publicKey };
|
|
}
|
|
|
|
function extractPublicKey(authData: Uint8Array) {
|
|
// Extract the public key from authData using appropriate parsing
|
|
// This involves extracting the COSE key format and converting it to JWK
|
|
// For simplicity, we'll assume the public key is at a certain position in authData
|
|
const publicKeyCose = authData.slice(authData.length - 77); // Example position
|
|
const publicKeyJwk = coseToJwk(publicKeyCose);
|
|
return publicKeyJwk;
|
|
}
|
|
|
|
function coseToJwk(coseKey: Uint8Array) {
|
|
// Convert COSE key format to JWK
|
|
// This is simplified and needs appropriate parsing and conversion logic
|
|
return {
|
|
kty: "EC",
|
|
crv: "P-256",
|
|
x: btoa(coseKey.slice(2, 34)),
|
|
y: btoa(coseKey.slice(34, 66)),
|
|
};
|
|
}
|
|
|
|
export async function createJwt(
|
|
payload: object,
|
|
issuerDid: string,
|
|
credentialId: ArrayBuffer,
|
|
) {
|
|
const signer = await webAuthnES256KSigner(credentialId);
|
|
|
|
// from createJWT in did-jwt/src/JWT.ts
|
|
const header: JWTPayload = { typ: "JWT", alg: "ES256K" };
|
|
const timestamps: Partial<JWTPayload> = {
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: undefined,
|
|
};
|
|
const fullPayload = { ...timestamps, ...payload, iss: issuerDid };
|
|
|
|
return createJWS(fullPayload, signer, header);
|
|
}
|
|
|
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) {
|
|
return async (data: string | Uint8Array) => {
|
|
// also has clientDataJSON
|
|
const { signature, authenticatorData } = await generateWebAuthnSignature(
|
|
data,
|
|
credentialID,
|
|
);
|
|
|
|
// Combine the WebAuthn components into a single signature format as required by did-jwt
|
|
const combinedSignature = Buffer.concat([
|
|
Buffer.from(authenticatorData),
|
|
Buffer.from(signature),
|
|
]);
|
|
|
|
return combinedSignature.toString("base64");
|
|
};
|
|
}
|
|
|
|
async function generateWebAuthnSignature(
|
|
dataToSign: string | Uint8Array,
|
|
credentialId: ArrayBuffer,
|
|
) {
|
|
if (!(dataToSign instanceof Uint8Array)) {
|
|
dataToSign = new TextEncoder().encode(dataToSign as string);
|
|
}
|
|
const challenge = generateRandomBytes(32);
|
|
|
|
const options = {
|
|
challenge: challenge,
|
|
allowCredentials: [{ id: credentialId, type: "public-key" }],
|
|
userVerification: "preferred",
|
|
};
|
|
|
|
const assertion = await navigator.credentials.get({ publicKey: options });
|
|
|
|
const authenticatorAssertionResponse = assertion?.response;
|
|
return {
|
|
signature: authenticatorAssertionResponse.signature,
|
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON,
|
|
authenticatorData: authenticatorAssertionResponse.authenticatorData,
|
|
};
|
|
}
|
|
|
|
export async function verifyJwt(jwt: string, publicKey: JWK) {
|
|
const decoded = verifyJWT(jwt, {
|
|
didAuthenticator: {
|
|
authenticators: [{ publicKeyJwk: publicKey }],
|
|
},
|
|
resolver: getResolver(),
|
|
});
|
|
return decoded;
|
|
}
|