Matt’s Headroom | Encrypting Knowledge within the Browser Utilizing WebAuthn
Encrypting Knowledge within the Browser Utilizing WebAuthn
– 8 minute learn
Once I found WebAuthn three years in the past a unusual thought got here to me: “what when you may additionally defend knowledge with a safety key?” The thought of a bodily authenticator getting used to encrypt and decrypt data caught with me, even after I got here to know that WebAuthn couldn’t be utilized in that means.
Quick ahead to 2023. The current addition of the prf
extension to the WebAuthn L3 Draft spec is introducing performance to WebAuthn that makes my loopy thought potential! Think about it: a fast faucet to encrypt a brilliant secret message, a brief journey by way of sneakernet, then a fast faucet to decrypt the message…
I’m pleased to report that my “loopy thought” has turn into a actuality. And even higher, it may all be completed solely within the browser ????
Disclaimer: for as in-depth as I can communicate to the sensible use of cryptographic ideas vis-a-vis WebAuthn, I’m nonetheless early in my training of many different fundamentals of cryptography. This publish represents my deepest dive but into extra advanced ideas like HMACs, key derivation, and encryption. Whereas I took steps to confirm the content material of this publish, I apologize for any inaccuracies. Please be happy to contact me if I’m off the mark on something.
## A abstract of the prf
WebAuthn extension
Briefly, prf
passes bytes from the “Relying Celebration” (that’s you, utilizing WebAuthn) to the authenticator throughout a WebAuthn authentication ceremony. The authenticator “hashes” (HMACs) these bytes with secret bytes internally related to a beforehand registered credential (as per CTAP’s hmac-secret
extension) and returns the ensuing bytes to the browser within the output from navigator.credentials.get()
.
The high-entropy bytes returned from the authenticator are excellent enter key materials for an “HMAC-based Key Derivation Perform” (HKDF), which helps us generate a “key derivation key”. The important thing derivation secret’s then used to derive one other symmetric key that’s used to carry out the precise knowledge encryption.
One thing to recollect is that output from the prf
extension shall be the identical for each authentication ceremony as long as A) the identical WebAuthn credential is used, and B) the bytes the RP passes to the authenticator are the identical. These two come collectively to make it potential to deterministically recreate the symmetric encryption key defending the information at any time. And even higher, the key bytes throughout the authenticator are origin-bound as properly due to the origin-bound credential they’re related to!
## Sensible use
You may observe alongside in just some steps:
- Set up Chrome Canary (at the very least Model 111.0.5548.0)
- Navigate to
chrome://flags/#enable-experimental-web-platform-features
and allow it - Seize a FIDO2 safety key manufactured within the final couple of years.
hmac-secret
exists in CTAP as early as 2018 so that you shouldn’t want the newest and biggest. I used a YubiKey Security Key for this.
Let’s get all the way down to brass tacks.
### Step 1: Register to prime the authenticator
Make a typical name to navigator.credentials.create()
with the prf
extension outlined:
/**
* This worth is for sake of demonstration. Choose 32 random
* bytes. `salt` will be static to your website or distinctive per
* credential relying in your wants.
*/
const firstSalt = new Uint8Array(new Array(32).fill(1)).buffer;
const regCredential = await navigator.credentials.create({
publicKey: {
problem: new Uint8Array([1, 2, 3, 4]), // Instance worth
rp: {
identify: "SimpleWebAuthn Instance",
id: "dev.dontneeda.pw",
},
person: {
id: new Uint8Array([5, 6, 7, 8]), // Instance worth
identify: "[email protected]",
displayName: "[email protected]",
},
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,
},
},
},
},
});
NOTE: The
first
handed in right here isn’t at present used throughout registration, however theprf
extension requires it to be set.
Faucet your safety key, observe the browser prompts, then name getClientExtensionResults()
afterwards and search for a prf
entry:
console.log(regCredential.getClientExtensionResults());
// {
// prf: {
// enabled: true
// }
// }
Should you see enabled: true
then you definitely’re good to proceed. Should you don’t then you definitely’ll must strive it once more with one other safety key till you discover one which works.
### Step 2: Authenticate to encrypt
The subsequent step is to name navigator.credentials.get()
and go in our firstSalt
:
const auth1Credential = await navigator.credentials.get({
publicKey: {
problem: new Uint8Array([9, 0, 1, 2]), // Instance worth
allowCredentials: [
{
id: regCredential.rawId,
transports: regCredential.response.getTransports(),
type: "public-key",
},
],
rpId: "dev.dontneeda.pw",
// This should at all times be both "discouraged" or "required".
// Choose one and keep it up.
userVerification: "required",
extensions: {
prf: {
eval: {
first: firstSalt,
},
},
},
},
});
Faucet your safety key once more, then name getClientExtensionResults()
afterwards and search for the prf
entry:
const auth1ExtensionResults = auth1Credential.getClientExtensionResults();
console.log(auth1ExtensionResults);
// prf: {
// outcomes: {
// first: ArrayBuffer(32),
// }
// }
// }
The first
bytes returned listed below are the important thing (no pun meant) to the following steps involving WebCrypto’s SubtleCrypto browser API:
#### Step 2.1: Import the enter key materials
Create a key derivation key utilizing crypto.delicate.importKey()
:
const inputKeyMaterial = new Uint8Array(
auth1ExtensionResults.prf.outcomes.first,
);
const keyDerivationKey = await crypto.delicate.importKey(
"uncooked",
inputKeyMaterial,
"HKDF",
false,
["deriveKey"],
);
#### Step 2.2: Derive the encryption key
Subsequent, create the symmetric key that we’ll use for encryption with crypto.delicate.deriveKey()
:
// Always remember what you set this worth to or the important thing cannot be
// derived later
const label = "encryption key";
const data = textEncoder.encode(label);
// `salt` is a required argument for `deriveKey()`, however ought to
// be empty
const salt = new Uint8Array();
const encryptionKey = await crypto.delicate.deriveKey(
{ identify: "HKDF", data, salt, hash: "SHA-256" },
keyDerivationKey,
{ identify: "AES-GCM", size: 256 },
// No want for exportability as a result of we will deterministically
// recreate this key
false,
["encrypt", "decrypt"],
);
#### Step 2.3: Encrypt the message
Now we will encrypt our message utilizing the aptly named crypto.delicate.encrypt()
technique:
// Maintain monitor of this `nonce`, you may want it to decrypt later!
// FYI it isn't a secret so you do not have to guard it.
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.delicate.encrypt(
{ identify: "AES-GCM", iv: nonce },
encryptionKey,
new TextEncoder().encode("howdy readers ????"),
);
### Step 3: Authenticate to decrypt
Decrypting the message appears to be like nearly the identical as the whole lot in Step 2, besides over the past step you’ll name crypto.delicate.decrypt()
as an alternative:
const decrypted = await crypto.delicate.decrypt(
// `nonce` ought to be the identical worth from Step 2.3
{ identify: "AES-GCM", iv: nonce },
encryptionKey,
encrypted,
);
Should you did the whole lot proper, you must see your tremendous secret message:
console.log((new TextDecoder()).decode(decrypted));
// howdy readers ????
## Proof
Right here’s a screenshot of Chrome Canary after I wired all of this up into my SimpleWebAuthn example server:
## Issues to recollect
- The
prf
extension is at present out there in Chrome Canary 111.In response to the Chrome Roadmap we will most likely anticipate to see(Edit: I’ve been knowledgeable that there’s extra work to be completed earlier thanprf
help roll out to everybody when Chrome 111 debuts round March 2023.prf
could make it into mainline Chrome so it will likely be some time but.) - Although the encryption key will be deterministically recreated, the bytes used to derive it are the results of a hash of
bytes throughout the authenticator || bytes offered by the RP
(see here for the way the browser truly saltsbytes offered by the RP
earlier than passing them to the authenticator). An attacker will simply seebytes offered by the RP
because the inputs for theprf
extension. Nonetheless they shouldn’t ever be capable of get tobytes throughout the authenticator
, making it safer to carry out the precise encryption and decryption within the browser. - This encryption scheme is basically shielded from distant threats due to WebAuthn’s phishing resistance. It is because the authenticator associates its contribution to the enter key materials to a selected origin-bound credential.
- Native threats like JavaScript injection assaults may exfiltrate the worth of
inputKeyMaterial
from Step 2.1 and retailer it away for later use. The Content Security Policy HTTP header can assist management what JavaScript executes in your website and cut back the opportunity of this occurring. Sadly it gained’t defend in opposition to malicious browser extensions which are sometimes capable of ignore CSP headers. - Consumer verification ought to at all times be
"required"
, or at all times be"discouraged"
. The authenticator makes use of two units of bytes withhmac-secret
, and chooses which to make use of primarily based on the"userVerification"
argument passed intonavigator.credentials.get()
. See references to “CredRandomWithUV” and “CredRandomWithoutUV” in the CTAP spec for more information. - I like to recommend you at all times require person verification. This protects your secret knowledge with the safety key’s PIN as properly for the reason that PIN shall be wanted to finish the WebAuthn authentication ceremonies.
- The
nonce
worth should at all times be distinctive for any single encryption (and its corresponding decryption) when utilizing AES-GCM encryption keys like within the code above. I’ve discovered it’s not a secret, although, so it may be safely transported with the encrypted knowledge for later decryption. - I protected a easy UTF-8 string within the instance above, however the encryption and decryption ought to work effective over any arbitrary bytes.
- There isn’t something stopping platform authenticators from supporting
prf
, however I haven’t discovered one which does but. I’ll replace this publish if/when any begin supporting it.
## In Conclusion
So there you will have it, knowledge encryption utilizing WebAuthn. I’m excited by the probabilities this brings to web sites, and I do know that it’s only a matter of time earlier than others discover novel methods to use this system to strongly defend your secrets and techniques.
And heck, now that I’ve written this I would simply strive creating one thing novel with prf
myself…
(An enormous due to Cendyne for serving to proof-read the cryptographic-heavy components of this publish!)