In AES-CBC terms, the algorithm can be described as:
1. L = AES-CBC-256ₖ(iv = 0¹²⁸, plaintext = 0¹²⁸)[:16]
2. If MSB₁(L) = 0, then K1 = L << 1;
Else K1 = (L << 1) ⊕ 0¹²⁰10000111
3. M1 = 0x00 || 0x01 || X || 0x00 || N[:12]
4. M2 = 0x00 || 0x02 || X || 0x00 || N[:12]
5. Kₓ = AES-CBC-256ₖ(iv = K1, plaintext = M1)[:16] || AES-CBC-256ₖ(iv = K1, plaintext = M2)[:16]
6. Nₓ = N[12:]
Where AES-CBC-256 returns the first 128-bit block of the ciphertext, discarding the padded block. (Thus, if you can't turn off padding, it costs three additional AES calls with the same key compared to a lower level implementation — not bad). After deriving a key, use it with the standard AES-GCM.Here's my JS implementation based on WebCrypto API, which uses this fact: https://github.com/dchest/xaes
It accepts a proper CryptoKey intended for AES-CBC, supporting all CryptoKey features, e.g. storing it in IndexedDB with "extractable" bit set to false.
Great job, Filippo!