Compatibility
Published
Cryptographic code attracts caution, and rightly so: a build step that quietly changed a key derivation would be a very expensive bug to find. So we did not reason about it. We ran real digests, a real HMAC and a real AES-GCM round trip through a real WebCrypto implementation, before and after protection, and compared the hex.
What was measured, against the real API
This sample does not model the platform. It calls node's WebCrypto implementation, which is the same specification the browser implements, so the numbers below are produced by the actual algorithms rather than by a stand-in.
It computes a SHA-256 digest of a fixed string, imports a raw HMAC key with an algorithm dictionary of { name: "HMAC", hash: { name: "SHA-256" } }, signs and verifies a payload, imports an AES-GCM key, encrypts with a fixed twelve-byte initialisation vector and a tag length, decrypts back to the original plaintext, and finally exercises getRandomValues and randomUUID for their shapes rather than their values.
All five configurations produced output identical to the original, line for line. The SHA-256 digest of the fixed input started 2cf24dba5fb0a30e before and after every configuration, the HMAC came out identical and verified, and the AES-GCM ciphertext was byte-identical with the plaintext recovered intact. The key object still reported type of secret, an algorithm name of HMAC, its two usages, and extractable false.
That result is not luck. WebCrypto is reached through built-in names on a built-in object, the arguments are ordinary values, and none of the transforms has a reason to touch either. The output is deterministic, so a single changed byte anywhere in the pipeline would have shown up as a diff.
The one thing that does break it
Member renaming, pointed at the wrong names, breaks WebCrypto three different ways, and we measured all three against the real implementation.
Point a member pattern at the algorithm dictionary keys, say ^(name|hash|iv|tagLength)$, and the platform rejects the call outright: Failed to normalize algorithm: passed algorithm can not be converted to 'Algorithm'. This is the good failure. The dictionary you passed no longer has the keys the specification requires, the platform notices immediately, and the error names the problem.
Point it at the key object instead, ^(algorithm|usages|extractable)$, and the failure moves one step later and gets quieter: key.algorithm reads as undefined, and the TypeError arrives only when something dereferences it. Code that merely logs those fields would produce undefined in a log line and carry on.
Point it at the method names, ^(digest|importKey|sign|verify|encrypt|decrypt)$, and the call site is rewritten to a member that SubtleCrypto does not have, so the very first cryptographic operation fails.
The rule underneath all three is the one this site keeps arriving at from different directions: renaming is safe when a name is reached only by code you control, and unsafe when the name is part of a contract with the language, a library or the platform. An algorithm dictionary is a contract with the platform. It happens to look like your own object, which is exactly why it gets caught by a loose pattern.
Writing a member pattern that leaves crypto alone
The practical advice is to make the pattern name your own fields explicitly rather than excluding the platform's, because the platform's surface is long and you will not remember all of it. A pattern anchored to the fields of your own models is both safer and easier to review than one that starts broad and subtracts.
If you would rather work by exclusion, the names to keep out of any pattern that runs over code touching WebCrypto are the dictionary keys name, hash, iv, salt, info, counter, length, tagLength, additionalData, namedCurve, modulusLength, publicExponent and iterations; the key surface type, algorithm, usages and extractable; and the method names on crypto.subtle.
Note how many of those are ordinary English words that also appear as fields on your own objects. name, length, type and info are the four that most often pull a pattern into trouble, and they are worth checking first when a protected build fails somewhere near cryptography.
Where the runtime defense options touch this
One entry on the anti-tampering option's default watch list is crypto.subtle.digest. The list also carries Function, Function.prototype.toString, eval, JSON.stringify and JSON.parse, several Array and Object methods, Promise.prototype.then, setTimeout, fetch, XMLHttpRequest, WebSocket, EventTarget.prototype.addEventListener, navigator.sendBeacon and the two Storage accessors.
It watches digest only, not encrypt, sign or importKey. So the option notices if something replaces the hashing function it uses, which is the one a page's own integrity checks are most likely to route through, and it says nothing about the rest of the cryptographic surface. If you want the rest watched, the option takes an explicit include list and an exclude list; the escape hatches are documented on the runtime defense page.
It is worth being precise about what watching means here. The check compares the function identity and its source text against what it captured at load, and treats a mismatch as tampering. That detects a naive replacement of a global. It is not a guarantee about the cryptography itself, and it does not turn a browser into a trusted environment.
The boundary this page will not cross
Everything above is a compatibility answer. It says the build step leaves your cryptography alone, which is what a build step should do, and it does not say anything about whether the design around that cryptography is sound.
Two things remain true whatever the protection settings are. A key that reaches the browser is available to whoever controls the browser, and marking a CryptoKey non-extractable limits copying the key material rather than limiting use of the key by script on the same origin. Protection changes how hard the surrounding code is to read; it does not move the trust boundary.
Those questions have their own pages on this site, and the honest split is worth keeping: this page tells you the algorithms still produce the same bytes, and the pages linked below tell you what the browser can and cannot be trusted to hold.
Frequently asked questions
Does obfuscation change the output of crypto.subtle?
No, and this was measured against a real WebCrypto implementation rather than a stand-in. A SHA-256 digest, an HMAC signature, and an AES-GCM ciphertext were byte-identical before and after protection in all five configurations we ran, with the AES-GCM plaintext recovered intact and the HMAC verifying.
Can member renaming break the Web Crypto API?
Yes, and it is the only thing in the pipeline that does. We measured three distinct failures: renaming the algorithm dictionary keys makes the platform reject the call with a normalisation error, renaming the CryptoKey fields makes key.algorithm read as undefined, and renaming the SubtleCrypto method names makes the first operation fail. Scope your member pattern to your own field names.
Which property names should stay out of a member renaming pattern?
The algorithm dictionary keys such as name, hash, iv, salt, length, tagLength, namedCurve and iterations; the key surface type, algorithm, usages and extractable; and the crypto.subtle method names. The four worth checking first are name, length, type and info, because they are ordinary words that also appear on application objects.
Does the anti-tampering option watch cryptographic functions?
It watches crypto.subtle.digest, which is on the documented default list along with Function, Function.prototype.toString, eval, the JSON methods, fetch, XMLHttpRequest, WebSocket, addEventListener, sendBeacon and the Storage accessors. It does not watch encrypt, sign or importKey by default. The option accepts an explicit include list if you want more of the surface covered.
Does obfuscating my code make browser-side cryptography trustworthy?
No, and this page will not claim it. A key delivered to a browser is available to whoever controls that browser, and a non-extractable CryptoKey limits copying the key material rather than limiting use of the key by script on the same origin. Protection changes how readable the surrounding code is; it does not move the trust boundary.
What should I check on a protected release that uses WebCrypto?
Run one known-answer test through the protected bundle: a fixed input, a fixed key, a fixed initialisation vector, compared against a hex value you recorded from the unprotected build. It takes minutes, it covers the algorithm dictionary, the key surface and the method names in one shot, and it is the only check this area really needs.
Related reading