AES-GCM is the default authenticated cipher of the modern web. It is fast, it is everywhere, and it has one failure mode that is unusually unforgiving.
Never reuse a nonce
GCM takes a nonce, a number used once. Encrypt two different messages with the same key and the same nonce, and an attacker who sees both can recover the XOR of the plaintexts.
That would be bad enough. It is worse: nonce reuse also leaks the authentication subkey. Once an attacker holds it, they can forge valid tags for messages you never sent, for that key, from then on. Confidentiality is dented; authenticity is destroyed.
There is no recovering from it by rotating the nonce afterwards. The key is finished.
How it happens in practice
- A counter reset. A process restarts, the counter goes back to zero, the key did not change.
- The same key on two machines, each with its own counter, both starting from the same place.
- A weak random source. A 96-bit random nonce is safe; a nonce drawn from something predictable is not random at all.
- A restored backup, replaying a range of nonces that were already used.
Every one of those is an operational accident, not a cryptographic one. That is what makes them likely.
Random, and rotate
Use 96 bits from a cryptographic random source, and rotate the key well before the birthday bound makes a collision plausible. In JOSE this is handled for you: every JWE carries a fresh content encryption key, so the same key is never used twice by construction.
That is a design decision worth noticing. The specification removed the footgun by making the dangerous case impossible rather than by warning about it.
A note of history
php-aes-gcm implements GCM in pure PHP. It was written when PHP had no native AEAD, before openssl_encrypt() learned to return a tag. Today the extension does the work, and the library remains useful where that extension is not available, or as something to read: an implementation you can follow line by line is the shortest path to understanding why the nonce matters so much.