PRF tester
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment