Skip to content

Commit

Permalink
JSON viewer for registration
Browse files Browse the repository at this point in the history
  • Loading branch information
agektmr committed Aug 27, 2024
1 parent 077dda2 commit a8eb7d6
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 12 deletions.
33 changes: 33 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@material/mwc-top-app-bar-fixed": "^0.27.0",
"@simplewebauthn/server": "^10.0.1",
"aaguid": "git+https://github.com/agektmr/passkey-authenticator-aaguids.git",
"cbor": "^9.0.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-handlebars": "^8.0.1",
Expand Down
120 changes: 110 additions & 10 deletions src/public/scripts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { base64url } from './base64url';
import { MDCRipple } from '@material/ripple';
import { initializeApp } from 'firebase/app';
import { Checkbox } from '@material/mwc-checkbox';
import cbor from 'cbor';
import * as firebaseui from 'firebaseui';
import {
getAuth,
Expand All @@ -43,6 +44,7 @@ import {
import { IconButton } from '@material/mwc-icon-button';
import { StoredCredential } from './common';

const BASE64_SLICE_LENGTH = 40;
const aaguids = await fetch('/webauthn/aaguids').then(res => res.json());

const app = initializeApp({
Expand Down Expand Up @@ -273,6 +275,109 @@ const rippleCard = (credID: string) => {
ripple.deactivate();
}

async function parseRegistrationCredential(
cred: RegistrationCredential
): Promise<any> {
const credJSON = {
id: cred.id.slice(0, BASE64_SLICE_LENGTH)+'...',
rawId: cred.id.slice(0, BASE64_SLICE_LENGTH)+'...',
type: cred.type,
response: {
clientDataJSON: {},
attestationObject: {
fmt: 'none',
attStmt: {},
authData: {},
},
transports: <any>[],
},
clientExtensionResults: {},
};

const decoder = new TextDecoder('utf-8');
credJSON.response.clientDataJSON = JSON.parse(decoder.decode(cred.response.clientDataJSON));
const attestationObject = cbor.decodeAllSync(cred.response.attestationObject)[0]

attestationObject.authData = await parseAuthenticatorData(attestationObject.authData);
credJSON.response.attestationObject = attestationObject;

if (cred.response.getTransports) {
credJSON.response.transports = cred.response.getTransports();
}

credJSON.clientExtensionResults = parseClientExtensionResults(cred);

return credJSON;
};

async function parseAuthenticatorData(
buffer: any
): Promise<any> {
const authData = {
rpIdHash: '',
flags: {
up: false,
uv: false,
be: false,
bs: false,
at: false,
ed: false,
},
counter: 0,
aaguid: '',
credentialID: '',
credentialPublicKey: '',
};

const rpIdHash = buffer.slice(0, 32);
buffer = buffer.slice(32);
authData.rpIdHash = rpIdHash.toString('hex');

const flags = (buffer.slice(0, 1))[0];
buffer = buffer.slice(1);
authData.flags = {
up: !!(flags & (1 << 0)),
uv: !!(flags & (1 << 2)),
be: !!(flags & (1 << 3)),
bs: !!(flags & (1 << 4)),
at: !!(flags & (1 << 6)),
ed: !!(flags & (1 << 7)),
};

const counter = buffer.slice(0, 4);
buffer = buffer.slice(4);
authData.counter = counter.readUInt32BE(0);

if (authData.flags.at) {
authData.aaguid = base64url.encode(buffer.slice(0, 16));
buffer = buffer.slice(16);

const credIDLenBuf = buffer.slice(0, 2);
buffer = buffer.slice(2);
const credIDLen = credIDLenBuf.readUInt16BE(0)
authData.credentialID = (base64url.encode(buffer.slice(0, credIDLen))).slice(0, BASE64_SLICE_LENGTH)+'...';
buffer = buffer.slice(credIDLen);

const firstDecoded = cbor.decodeFirstSync(buffer.slice(0));
authData.credentialPublicKey = base64url.encode(Uint8Array.from(cbor.encode(firstDecoded)));
}

return authData;
}

function parseClientExtensionResults(
credential: RegistrationCredential
): AuthenticationExtensionsClientOutputs {
const clientExtensionResults: AuthenticationExtensionsClientOutputs = {};
if (credential.getClientExtensionResults) {
const extensions: AuthenticationExtensionsClientOutputs = credential.getClientExtensionResults();
if (extensions.credProps) {
clientExtensionResults.credProps = extensions.credProps;
}
}
return clientExtensionResults;
}

/**
* Fetch and render the list of credentials.
*/
Expand Down Expand Up @@ -425,12 +530,14 @@ const registerCredential = async (opts: WebAuthnRegistrationObject): Promise<any
clientExtensionResults,
} as RegistrationResponseJSON;

onViewPayload(encodedCredential);
const parsedCredential = await parseRegistrationCredential(credential);

console.log('[AttestationCredential]', encodedCredential);

// Verify and store the attestation.
await _fetch('/webauthn/registerResponse', encodedCredential);

return parsedCredential;
};

/**
Expand Down Expand Up @@ -613,8 +720,8 @@ const onRegisterNewCredential = async (): Promise<void> => {
loading.start();
const opts = <WebAuthnRegistrationObject>collectOptions('registration');
try {
await registerCredential(opts);
showSnackbar('A credential successfully registered!');
const parsedCredential = await registerCredential(opts);
showSnackbar('A credential successfully registered!', parsedCredential);
listCredentials();
} catch (e: any) {
console.error(e);
Expand Down Expand Up @@ -668,13 +775,6 @@ const onAuthenticate = async (): Promise<void> => {
}
};

const onViewPayload = async (
payload: JSON
): Promise<void> => {
$('#json-viewer').data = { payload };
$('#payload-viewer').show();
};

const onTxAuthSimpleSiwtch = async (): Promise<void> => {
$('#tx-auth-simple').disabled = $('#switch-tx-auth-simple').checked;
}
Expand Down
22 changes: 21 additions & 1 deletion src/public/scripts/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,28 @@ const $: any = document.querySelector.bind(document);

const snackbar = $('#snackbar') as Snackbar;

function showSnackbar(message: string): void {
async function viewPayload(
payload: JSON
): Promise<void> {
$('#json-viewer').data = { payload };
$('#json-viewer').expandAll();
$('#payload-viewer').show();
};

function showSnackbar(message: string, payload?: any): void {
snackbar.labelText = message;
if (payload) {
const button = document.createElement('mwc-button');
button.id = 'snack-button';
button.slot = 'action';
button.innerText = 'Show payload';
button.addEventListener('click', e => {
viewPayload(payload);
});
snackbar.appendChild(button);
} else {
$('#snack-button')?.remove();
}
snackbar.show();
};

Expand Down
4 changes: 3 additions & 1 deletion src/templates/layouts/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</head>
<body>
{{{body}}}
<mwc-snackbar id="snackbar"></mwc-snackbar>
<mwc-snackbar id="snackbar">
<mwc-icon-button icon="close" slot="dismiss"></mwc-icon-button>
</mwc-snackbar>
</body>
</html>

0 comments on commit a8eb7d6

Please sign in to comment.