Skip to content

Commit

Permalink
feat: work in progress for extracting public key, see logs
Browse files Browse the repository at this point in the history
  • Loading branch information
yum0e committed Oct 26, 2023
1 parent e60e3ed commit 89f096e
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 68 deletions.
2 changes: 2 additions & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"@simplewebauthn/browser": "^8.3.3",
"@simplewebauthn/typescript-types": "^8.3.3",
"@walletconnect/core": "^2.10.4",
"cbor": "^9.0.1",
"next": "^13.4.0",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"viem": "~0.3.36",
Expand Down
30 changes: 30 additions & 0 deletions front/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 27 additions & 27 deletions front/src/components/PassKey.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
"use client";

import { useEffect, useState } from "react";
import WebAuthn from "../libs/webauthn";
import WebAuthn, { CreateCredential, P256Credential } from "../libs/webauthn";
import { stringify } from "@/utils/stringify";
import { create } from "domain";
import { Hex } from "viem";

const webauthn = new WebAuthn();

function arrayBufferToString(buffer: ArrayBuffer) {
let str = "";
const array = new Uint8Array(buffer);
for (let i = 0; i < array.length; i++) {
str += String.fromCharCode(array[i]);
}
return str;
}

export default function PassKey() {
const [username, setUsername] = useState<string>("super-user");
const [credential, setCredential] = useState<Credential | null>(null);
const [clientDataJson, setClientDataJson] = useState<string | null>(null);
const [createCredential, setCreateCredential] = useState<{
rawId: Hex;
pubKey: CryptoKey;
} | null>(null);
const [credential, setCredential] = useState<P256Credential | null>(null);

function onUsernameChange(event: React.ChangeEvent<HTMLInputElement>) {
setUsername(event.target.value);
}

async function onCreate() {
setCredential(await webauthn.create({ username }));
let credential = await webauthn.create({ username });
let pubKey: CryptoKey = await crypto.subtle.importKey(
"spki",
credential?.pubKey as ArrayBuffer,
{ name: "ECDSA", namedCurve: "P-256" },
true,
["verify"],
);

console.log("PUB KEY", await crypto.subtle.exportKey("jwk", pubKey));

setCreateCredential({
rawId: credential?.rawId as Hex,
pubKey,
});
}

async function onGet() {
setCredential(await webauthn.get());
}

useEffect(() => {
let cred = credential as unknown as { response: { clientDataJSON: ArrayBuffer } };
console.log("cred", cred);
let clientDataJson = arrayBufferToString(cred?.response?.clientDataJSON);
setClientDataJson(clientDataJson);
let challenge = JSON.parse(JSON.stringify(clientDataJson)) as { challenge: string };
console.log("client data json", clientDataJson);
console.log("challenge", challenge?.challenge);
}, [credential, clientDataJson]);

return (
<>
<div style={{ content: "center", margin: 20 }}>
Expand All @@ -54,11 +55,10 @@ export default function PassKey() {
<button onClick={onCreate}>Create</button>
<button onClick={onGet}>Get</button>
</div>
{credential && (
<div style={{ content: "center", margin: 20 }}>
ClientDataJSON: {JSON.stringify(clientDataJson, null, 2)}
</div>
{createCredential && (
<div style={{ content: "center", margin: 20 }}>{stringify(createCredential)}</div>
)}
{credential && <div style={{ content: "center", margin: 20 }}>{stringify(credential)}</div>}
</>
);
}
197 changes: 156 additions & 41 deletions front/src/libs/webauthn/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,107 @@
import crypto from "crypto";
import { Hex, Signature, toHex } from "viem";
import cbor from "cbor";

function arrayBufferToString(buffer: ArrayBuffer) {
let str = "";
const array = new Uint8Array(buffer);
for (let i = 0; i < array.length; i++) {
str += String.fromCharCode(array[i]);
}
return str;
}

function arrayBufferToHex(arrayBuffer: ArrayBuffer): string {
const uint8Array = new Uint8Array(arrayBuffer);
return Array.from(uint8Array)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}

function fromBase64ToHex(str: string) {
const buffer = Buffer.from(str, "base64");
return buffer.toString("hex");
}

function extractIntegersFromDERSignature(buffer: ArrayBuffer): { r: Hex; s: Hex } {
const data = new Uint8Array(buffer);

// Validate SEQUENCE
if (data[0] !== 0x30 || data.length < 2) {
throw new Error("Invalid DER signature");
}

// Retrieve length of R and S
const rLength = data[3];
const sLength = data[5 + rLength];

// Extract the R and S integers
const r = toHex(data.slice(4, 4 + rLength));
const s = toHex(data.slice(6 + sLength));

return { r, s };
}

function getPublicKey(authData: Uint8Array): { x: Hex; y: Hex } {
// get the length of the credential ID
const dataView = new DataView(new ArrayBuffer(2));
const idLenBytes = authData.slice(53, 55);
console.log("idLenBytes", idLenBytes);
idLenBytes.forEach((value, index) => {
console.log("data viexw", dataView);
dataView.setUint8(index, value);
});
console.log("DATA VIEW: ", dataView);
const credentialIdLength = dataView.getUint8(0);

// get the public key object
const publicKeyBytes = authData.slice(55 + credentialIdLength);

console.log("PUBLIC KEY BYTES: ", publicKeyBytes);

// the publicKeyBytes are encoded again as CBOR
const publicKeyObject: { "-2": Uint8Array; "-3": Uint8Array } = cbor.decode(
publicKeyBytes.buffer,
);

console.log("PUBLIC KEY OBJECT: ", publicKeyObject);

const aaa = authData.slice(37, 37 + 65);
console.log("AAA: ", aaa);

return {
x: toHex(publicKeyObject["-2"]),
y: toHex(publicKeyObject["-3"]),
};
}

export type CreateCredential = {
rawId: Hex;
pubKey: ArrayBuffer;
};

export type P256Credential = {
rawId: Hex;
clientData: {
type: string;
challenge: string;
origin: string;
};
authenticatorData: Hex;
signature: P256Signature;
};

export type P256Signature = {
r: Hex;
s: Hex;
};

export default class WebAuthn {
private _generateRandomBytes(): Buffer {
return crypto.randomBytes(16);
}

async create({ username }: { username: string }): Promise<Credential | null> {
async create({ username }: { username: string }): Promise<CreateCredential | null> {
const options: PublicKeyCredentialCreationOptions = {
timeout: 60000,
rp: {
Expand All @@ -18,70 +114,89 @@ export default class WebAuthn {
displayName: username,
},
pubKeyCredParams: [
{ alg: -8, type: "public-key" }, // Ed25519
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
],
authenticatorSelection: {
// authenticatorAttachment: "cross-platform",
requireResidentKey: true,
userVerification: "required",
},
// extensions: {
// prf: {
// eval: {
// first: new TextEncoder().encode("Foo encryption key").buffer,
// },
// },
// } as AuthenticationExtensionsClientInputs,
// Challenge is unused during registration, but must be present
challenge: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).buffer,
attestation: "direct",
challenge: Uint8Array.from("random-challenge", (c) => c.charCodeAt(0)),
};

const credential = await navigator.credentials.create({
publicKey: options,
});

if (!credential) {
return null;
}

console.log("CREATE CREDENTIAL: ", credential);
return credential;
let cred = credential as unknown as {
rawId: ArrayBuffer;
response: {
clientDataJSON: ArrayBuffer;
attestationObject: ArrayBuffer;
getPublicKey: () => any;
};
};

const decodedAttestationObj = cbor.decode(cred.response.attestationObject);
console.log("DECODED ATTESTATION OBJ: ", decodedAttestationObj);

const { authData } = decodedAttestationObj;
const pubKey = cred.response.getPublicKey();

console.log("pub key ", pubKey);

return {
rawId: toHex(new Uint8Array(cred.rawId)),
pubKey,
};
}

async get(): Promise<Credential | null> {
async get(): Promise<P256Credential | null> {
const options: PublicKeyCredentialRequestOptions = {
timeout: 60000,
challenge: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).buffer,
challenge: Uint8Array.from("random-challenge", (c) => c.charCodeAt(0)),
rpId: "localhost",
userVerification: "required",
extensions: {
prf: {
// hmacCreateSecret: true,
eval: {
first: new TextEncoder().encode("Foo encryption key").buffer,
},
},
} as AuthenticationExtensionsClientInputs,
};

const credential = await navigator.credentials.get({
publicKey: options,
});

// const result = btoa(
// String.fromCharCode.apply(
// null,
// new Uint8Array(
// credential.getClientExtensionResults().prf.results.first
// ) as any
// )
// );

// console.log(
// "CREDENTIAL: ",
// new TextDecoder("utf-8").decode(
// new Uint8Array((credential as any).response.clientDataJSON)
// )
// );
console.log("GET CREDENTIAL: ", credential);
return credential;
if (!credential) {
return null;
}

let cred = credential as unknown as {
rawId: ArrayBuffer;
response: {
clientDataJSON: ArrayBuffer;
authenticatorData: ArrayBuffer;
signature: ArrayBuffer;
};
};

const utf8Decoder = new TextDecoder("utf-8");
const decodedClientData = utf8Decoder.decode(cred.response.clientDataJSON);
const clientDataObj = JSON.parse(decodedClientData);

let authenticatorData = toHex(new Uint8Array(cred.response.authenticatorData));

let signature = extractIntegersFromDERSignature(cred?.response?.signature as ArrayBuffer);
return {
rawId: toHex(new Uint8Array(cred.rawId)),
clientData: {
type: clientDataObj.type,
challenge: clientDataObj.challenge,
origin: clientDataObj.origin,
},
authenticatorData,
signature,
};
}
}

0 comments on commit 89f096e

Please sign in to comment.