Encryption with pseudo random function (PRF)
This feature is not (yet) supported on your device
The WebAuthn PRF extension enables web apps to derive encryption keys using the user's passkey, without ever
accessing the private key itself. This ensures that the private key remains secure and never leaves the device.
With solutions like the Web Crypto API, a web app can also generate encryption keys, but they need to be stored in
localStorage or IndexedDB. However, these keys are not protected by the user's passkey and can be stolen if
the device is hacked. With the PRF extension, the private key never leaves the device and is never accessed by
the web app.
The web app can use the derived encryption keys to securely encrypt and decrypt data without ever having access
to the private key itself. This ensures that sensitive data remains protected even if the device is compromised.
After authentication with a passkey, the web app can generate a new encryption by providing a text label. Every
time the web app provides the same text label, the same encryption key is derived. This ensures that the same
encryption key is used for the same data, providing consistency and security.
Demo
Register a passkey, add some text to encrypt to the first text area and click "Encrypt text". The encrypted text
will be displayed in the second text area.
Refresh the page or close and reopen the app. Then authenticate with the passkey and the encrypted text will be
shown in the second text area again. Click the "Decrypt text" button to see the decrypted text in the first text
area.
After authentication, the demo code will regenerate the exact same key using the same text label. While using the
same key, you will see that the text is always correctly decrypted.
delete
autorenew
Code
// create the passkey and provide a text label to derive the encryption key
const credential = await navigator.credentials.create({
publicKey: {
challenge,
rp: { name: "Secure Notes" },
user: {
id: userIdBytes,
name: "pwa@whatpwacando.today",
displayName: "PWA"
},
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
authenticatorSelection: {
userVerification: "required"
},
extensions: {
prf: {
eval: {
first: new TextEncoder().encode("prf-key-v1") // the text label to derive the key
}
}
}
}
});
// get the secret to derive the key
const prfResult = credential.getClientExtensionResults().prf.results.first;
// create a CryptoKey object from the PRF result
const keyMaterial = await crypto.subtle.importKey(
"raw",
prfResult,
"HKDF",
false,
["deriveKey"]
);
// derive the actual encryption/decryption key
const encryptionKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array([]),
info: new TextEncoder().encode("prf-demo"),
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
// you can now encrypt text
// if you want to save this to a server, send the ciphertext and iv
// the key stays save on the user's device
const iv = crypto.getRandomValues(new Uint8Array(12));
// encrypt text
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
encryptionKey,
new TextEncoder().encode("My secret text") // the text to encrypt
);
// decrypt text
const decryptedText = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
encryptionKey,
ciphertext
);
// authenticate and derive the same key by providing the same text label
const assertion = await navigator.credentials.get({
publicKey: {
challenge,
allowCredentials: [{ id: credentialId, type: "public-key" }],
userVerification: "required",
extensions: {
prf: {
eval: {
first: new TextEncoder().encode("prf-key-v1") // same text label to derive the key
}
}
}
}
});
// derive the same key in the same way as above
const prfResult = credential.getClientExtensionResults().prf.results.first;
// create a CryptoKey object from the PRF result
const keyMaterial = await crypto.subtle.importKey(
"raw",
prfResult,
"HKDF",
false,
["deriveKey"]
);
// derive the same encryption/decryption key as with registration
const encryptionKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array([]),
info: new TextEncoder().encode("prf-demo"),
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);