Skip to content

Instantly share code, notes, and snippets.

@MasterKale
Last active May 16, 2024 15:00
Show Gist options
  • Save MasterKale/dbe39a01438251f0cbd55576304731fd to your computer and use it in GitHub Desktop.
Save MasterKale/dbe39a01438251f0cbd55576304731fd to your computer and use it in GitHub Desktop.
PRF tester
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
button {
font-size: 1.5em;
}
</style>
</head>
<body>
<h1>Open your dev console before clicking either of these!</h1>
<h2>FYI both steps are required on every reload, nothing persists.</h2>
<ol>
<li>
<button id="register">Register</button>
</li>
<li><h3>Check for "PRF supported: true" in the console before continuing</h3></li>
<li>
<button id="authenticate">Authenticate</button>
</li>
</ol>
<script>
/**
* A simple webpage to test a browser's PRF support.
*/
document.getElementById("register").addEventListener("click", register);
document.getElementById("authenticate").addEventListener("click", authenticate);
let regCredential;
const firstSalt = new Uint8Array(new Array(32).fill(1)).buffer;
async function register() {
regCredential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array([1, 2, 3, 4]), // Example value
rp: {
name: "localhost PRF demo",
id: "localhost",
},
user: {
id: new Uint8Array([5, 6, 7, 8]), // Example value
name: "user@localhost",
displayName: "user@localhost",
},
pubKeyCredParams: [
{ alg: -8, type: "public-key" }, // Ed25519
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
],
authenticatorSelection: {
userVerification: "required",
},
extensions: {
prf: {
eval: {
first: firstSalt,
},
},
},
},
});
const extensionResults = regCredential.getClientExtensionResults();
// Looking for something like this
// {
// prf: {
// enabled: true
// }
// }
const prfSupported = !!(
extensionResults.prf && extensionResults.prf.enabled
);
console.log(`PRF supported: ${prfSupported}`);
}
async function authenticate() {
const auth1Credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array([9, 0, 1, 2]), // Example value
allowCredentials: [
{
id: regCredential.rawId,
transports: regCredential.response.getTransports(),
type: "public-key",
},
],
rpId: "localhost",
// This must always be either "discouraged" or "required".
// Pick one and stick with it.
userVerification: "required",
extensions: {
prf: {
eval: {
first: firstSalt,
},
},
},
},
});
const auth1ExtensionResults =
auth1Credential.getClientExtensionResults();
console.log('Auth extension results:', auth1ExtensionResults);
const inputKeyMaterial = new Uint8Array(
auth1ExtensionResults.prf.results.first
);
const keyDerivationKey = await crypto.subtle.importKey(
"raw",
inputKeyMaterial,
"HKDF",
false,
["deriveKey"]
);
// Never forget what you set this value to or the key can't be
// derived later
const label = "encryption key";
const info = new TextEncoder().encode(label);
// `salt` is a required argument for `deriveKey()`, but should
// be empty
const salt = new Uint8Array();
const encryptionKey = await crypto.subtle.deriveKey(
{ name: "HKDF", info, salt, hash: "SHA-256" },
keyDerivationKey,
{ name: "AES-GCM", length: 256 },
// No need for exportability because we can deterministically
// recreate this key
false,
["encrypt", "decrypt"]
);
// Keep track of this `nonce`, you'll need it to decrypt later!
// FYI it's not a secret so you don't have to protect it.
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
encryptionKey,
new TextEncoder().encode("hello readers 🥳")
);
const decrypted = await crypto.subtle.decrypt(
// `nonce` should be the same value from Step 2.3
{ name: "AES-GCM", iv: nonce },
encryptionKey,
encrypted
);
const decodedMessage = new TextDecoder().decode(decrypted);
console.log(`Decoded message: "${decodedMessage}"`);
// hello readers 🥳
}
</script>
</body>
</html>
@ragnarbull
Copy link

hey @MasterKale I've run this in the latest version of Chrome (also in Chrome Canary) with a YubiKey 5C NFC (Firmware: 5.4.3) and extension results is giving: {"prf":{"enabled":false}} Bit confused as I thought recent YubiKeys have the HMAC secret and Chrome supports the prf extension from 116...

@dankelleher
Copy link

Me too ( prf enabled false, enable-experimental-web-platform-features switched on) - has Chrome disabled PRF even on canary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment