package com.dangerousthings.aliro; import javacard.framework.JCSystem; import javacard.framework.Util; import javacard.security.AESKey; import javacard.security.CryptoException; import javacard.security.KeyBuilder; import javacardx.crypto.Cipher; /** * Userland AES-256-GCM, built on top of the JC 3.0.5 single-block AES-ECB * primitive. Needed because the target card (NXP J3R180) does not expose * {@code AEADCipher.ALG_AES_GCM} despite advertising JC 3.0.5 support, while * {@code Cipher.ALG_AES_BLOCK_128_ECB_NOPAD} and AES-256 keys are both * present and functional. * *

Tailored to the Aliro AUTH1 response use case (spec §8.3.1.6): * AES-256 with a 12-byte IV, empty AAD, 16-byte tag, and plaintext on the * order of 137 bytes. The NIST SP 800-38D canonical 96-bit-IV shortcut is * used (IV is taken as {@code IV || 0x00000001} for J0, no IV * hashing). This is the only IV length we ever produce — see * {@link AliroApplet#encryptResponseGcm}. * *

GHASH follows NIST SP 800-38D §6.4's bit-reflected convention: * byte 0 bit 7 (MSB) is the highest-degree polynomial coefficient; the * reduction polynomial R = x^128 + x^7 + x^2 + x + 1 is represented as the * 16-byte array {0xE1, 0x00, ..., 0x00}; the GHASH shift-right shifts toward * higher byte indices (lower polynomial degrees). * *

Not constant-time — the GF(2^128) multiply branches on bits of the * authentication tag partial state. On a secure JC platform this is * acceptable (the key material is smartcard-protected), but callers should * not pass attacker-chosen GHASH state. */ final class AliroGcm { private static final short BLOCK_LEN = 16; private static final short TAG_LEN = 16; private static final short IV_LEN = 12; // 96-bit IV shortcut (NIST SP 800-38D §7.1) private static final short AES_256_KEY_BYTES = 32; // Scratch layout — all 16-byte slots, single transient allocation. private static final short OFF_H = 0; // hash subkey = AES_K(0^128) private static final short OFF_J0 = 16; // pre-counter block private static final short OFF_CB = 32; // running counter block private static final short OFF_GHASH = 48; // accumulated GHASH state private static final short OFF_ECB_OUT = 64; // AES keystream block (also used for J0 encryption) private static final short OFF_TMP = 80; // misc: GF multiply workspace (V) private static final short OFF_LEN_BLK = 96; // length block for GHASH finalization private static final short SCRATCH_LEN = 112; // 4-bit GHASH lookup tables. M is built per-H in mTable[0..256), L is the // fixed shift-right-by-4 reduction lookup (bytes 2..15 of each L[i] are // always zero for the NIST GHASH reduction polynomial R = 0xE1 00...00, // so only byte 0 and byte 1 are stored). private static final short M_TABLE_LEN = (short) (16 * BLOCK_LEN); // 256 B // L_BYTE0[i] / L_BYTE1[i] = byte 0 / byte 1 of (R XOR R>>1 XOR R>>2 XOR R>>3) // selected by which bits of nibble i are set: // bit 0 set -> XOR R >> 3 = 0x1C 0x20 // bit 1 set -> XOR R >> 2 = 0x38 0x40 // bit 2 set -> XOR R >> 1 = 0x70 0x80 // bit 3 set -> XOR R = 0xE1 0x00 private static final byte[] L_BYTE0 = { (byte)0x00, (byte)0x1C, (byte)0x38, (byte)0x24, (byte)0x70, (byte)0x6C, (byte)0x48, (byte)0x54, (byte)0xE1, (byte)0xFD, (byte)0xD9, (byte)0xC5, (byte)0x91, (byte)0x8D, (byte)0xA9, (byte)0xB5 }; private static final byte[] L_BYTE1 = { (byte)0x00, (byte)0x20, (byte)0x40, (byte)0x60, (byte)0x80, (byte)0xA0, (byte)0xC0, (byte)0xE0, (byte)0x00, (byte)0x20, (byte)0x40, (byte)0x60, (byte)0x80, (byte)0xA0, (byte)0xC0, (byte)0xE0 }; /** Persistent ECB cipher (AES-128/192/256 single-block-NOPAD). AES-256 * is driven by the size of the {@link AESKey} passed to {@link Cipher#init}. * Always allocated -- required for computing H = AES_K(0^128) and the * per-tag AES_K(J0), regardless of whether CTR is native or userland. */ private final Cipher aesEcb; /** Reusable 256-bit AES key slot. {@link AESKey#setKey} is called per encrypt. */ private final AESKey aesKey; /** Transient scratch — see OFF_* constants above. */ private final byte[] scratch; /** Transient 4-bit GHASH M-table: mTable[i*16..(i+1)*16) = M[i] = i_poly · H, * rebuilt at the start of every encrypt() once H is computed from the * fresh AES key. */ private final byte[] mTable; /** 1 if Cipher.ALG_AES_CTR is available on this JCRE (J3R452 etc.), 0 if * not (J3R180). When 1, the GCTR pass is one native doFinal call instead * of ceil(ptLen/16) block-by-block ECB calls -- saves the per-block * init/init/encrypt overhead. */ private final byte usesNativeCtr; /** Native CTR cipher; null when {@link #usesNativeCtr} == 0. */ private Cipher aesCtr; AliroGcm() { aesEcb = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_ECB_NOPAD, false); aesKey = (AESKey) KeyBuilder.buildKey( KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); scratch = JCSystem.makeTransientByteArray(SCRATCH_LEN, JCSystem.CLEAR_ON_DESELECT); mTable = JCSystem.makeTransientByteArray(M_TABLE_LEN, JCSystem.CLEAR_ON_DESELECT); // Native AES-CTR detection is temporarily disabled. On the J3R452 the // smoke test passed but the real encrypt path hangs (>60 s for what // should take ~22 s) -- something about Cipher.ALG_AES_CTR on that // runtime interacts badly with the GHASH loop that follows it (best // guess: shared AES engine state we're not flushing). Forcing the // ECB-block-by-block path keeps the behaviour identical to J3R180 // baseline; we'll revisit native CTR after the 4-bit GHASH table // lands and we have a baseline to compare against. usesNativeCtr = 0; } /** * AES-256-GCM encrypt with the Aliro parameters: 32-byte key, 12-byte IV, * empty AAD, 16-byte tag appended. Writes {@code ptLen} ciphertext bytes * followed by 16 tag bytes to {@code out[outOff..]} and returns * {@code ptLen + 16}. * *

The {@code out} buffer may overlap {@code pt} (same buffer, same * offset is supported — we encrypt block-by-block left-to-right, the * GHASH pass reads from {@code out}, and the tag lands past the end of * the ciphertext). * * @param key 32-byte AES-256 key, at {@code key[keyOff..keyOff+32)} * @param iv 12-byte IV, at {@code iv[ivOff..ivOff+12)} * @param pt plaintext bytes * @param ptOff, ptLen plaintext region * @param out output buffer, must have at least {@code ptLen + 16} bytes * available at {@code outOff} * @param outOff output start offset * @return {@code ptLen + 16} */ short encrypt( byte[] key, short keyOff, byte[] iv, short ivOff, byte[] pt, short ptOff, short ptLen, byte[] out, short outOff) { setKeyAndIv(key, keyOff, iv, ivOff); // cb = INC32(J0) — first counter block for the plaintext stream. Util.arrayCopyNonAtomic(scratch, OFF_J0, scratch, OFF_CB, BLOCK_LEN); inc32(scratch, OFF_CB); if (usesNativeCtr != 0) { // Native AES-CTR: single call encrypts the whole plaintext (handles // partial final block internally). Saves the per-block init/doFinal // overhead of the ECB fallback loop below. aesCtr.init(aesKey, Cipher.MODE_ENCRYPT, scratch, OFF_CB, BLOCK_LEN); aesCtr.doFinal(pt, ptOff, ptLen, out, outOff); } else { // GCTR fallback: stream plaintext through block-by-block ECB. short produced = 0; while (produced < ptLen) { short blockLen = (short) (ptLen - produced); if (blockLen > BLOCK_LEN) blockLen = BLOCK_LEN; // keystream = AES_K(cb) aesEcb.doFinal(scratch, OFF_CB, BLOCK_LEN, scratch, OFF_ECB_OUT); // ct[i] = pt[i] XOR keystream[i] — partial-block safe. for (short i = 0; i < blockLen; i++) { out[(short) (outOff + produced + i)] = (byte) (pt[(short) (ptOff + produced + i)] ^ scratch[(short) (OFF_ECB_OUT + i)]); } inc32(scratch, OFF_CB); produced += blockLen; } } // GHASH over (AAD=empty || ciphertext || len_block). // state = 0^128 Util.arrayFillNonAtomic(scratch, OFF_GHASH, BLOCK_LEN, (byte) 0); // Fold ciphertext blocks into GHASH, zero-padding the final partial block. short consumed = 0; while (consumed < ptLen) { short chunk = (short) (ptLen - consumed); if (chunk >= BLOCK_LEN) { // XOR full 16-byte block directly from the output buffer. for (short i = 0; i < BLOCK_LEN; i++) { scratch[(short) (OFF_GHASH + i)] ^= out[(short) (outOff + consumed + i)]; } consumed += BLOCK_LEN; } else { // Partial block: XOR the available bytes, implicit zero-pad for the rest. for (short i = 0; i < chunk; i++) { scratch[(short) (OFF_GHASH + i)] ^= out[(short) (outOff + consumed + i)]; } consumed += chunk; } // GHASH state · H using 4-bit table. gfMul4Bit writes to OFF_ECB_OUT; // copy result back into OFF_GHASH to match the existing protocol. gfMul4Bit(scratch, OFF_GHASH); Util.arrayCopyNonAtomic(scratch, OFF_ECB_OUT, scratch, OFF_GHASH, BLOCK_LEN); } // len_block = (8*|AAD|)^64 || (8*|C|)^64, big-endian. AAD is always empty // for this use case; ptLen fits in 16 bits, so only the low 2 bytes of // the 8-byte plaintext-bit-length field can be non-zero. Util.arrayFillNonAtomic(scratch, OFF_LEN_BLK, BLOCK_LEN, (byte) 0); // ptLen·8 in bits, big-endian, into the last 4 bytes of the 16-byte // length block (bits 0-95 are zero — AAD-bits=0 + high bytes of CT-bits). // Java Card 3.0.5 forbids `int` literals, so we compute as `short` and // split the byte-shift into two stages to dodge the implicit `int` // promotion that the converter rejects ("unsupported int type constant"). // ptLen ≤ 8191 ⇒ ptLen·8 ≤ 65528, fits in unsigned 16 bits. The top two // bytes of the length field stay zero. short ctBitsLo = (short) (ptLen << 3); // low 16 bits of bit-count short ctBitsHi = (short) (((short)(ptLen >>> 13)) & 0x07); // bits 16-18 of bit-count scratch[(short) (OFF_LEN_BLK + 13)] = (byte) (ctBitsHi & 0xFF); scratch[(short) (OFF_LEN_BLK + 14)] = (byte) ((ctBitsLo >>> 8) & 0xFF); scratch[(short) (OFF_LEN_BLK + 15)] = (byte) (ctBitsLo & 0xFF); for (short i = 0; i < BLOCK_LEN; i++) { scratch[(short) (OFF_GHASH + i)] ^= scratch[(short) (OFF_LEN_BLK + i)]; } gfMul(scratch, OFF_GHASH, scratch, OFF_H); // T = AES_K(J0) XOR GHASH aesEcb.doFinal(scratch, OFF_J0, BLOCK_LEN, scratch, OFF_ECB_OUT); short tagOut = (short) (outOff + ptLen); for (short i = 0; i < TAG_LEN; i++) { out[(short) (tagOut + i)] = (byte) (scratch[(short) (OFF_ECB_OUT + i)] ^ scratch[(short) (OFF_GHASH + i)]); } // Wipe scratch: H, J0 (with IV bytes), GHASH state, and the keystream // remnant in OFF_ECB_OUT all touched the AES key directly. CLEAR_ON_DESELECT // would clean up at deselect, but multi-APDU sessions could otherwise // see stale H/keystream until then. Util.arrayFillNonAtomic(scratch, (short) 0, SCRATCH_LEN, (byte) 0); return (short) (ptLen + TAG_LEN); } /** * AES-256-GCM decrypt with the Aliro parameters: 32-byte key, 12-byte IV, * empty AAD, 16-byte tag appended. Input is * {@code in[inOff..inOff+inLen)} laid out as * {@code ciphertext || tag} where the trailing 16 bytes are the tag and * the preceding {@code inLen - 16} bytes are the ciphertext. The tag is * verified before any plaintext is emitted; if verification fails the * method throws a {@link CryptoException} ({@code ILLEGAL_VALUE}) and does * NOT write to {@code out}. On success {@code inLen - 16} plaintext bytes * are written to {@code out[outOff..]} and the same value is returned. * *

The buffer-aliasing rules mirror {@link #encrypt}: {@code in} and * {@code out} may be the same buffer at the same offset (we GHASH the * ciphertext BEFORE we touch the output region, then GCTR overwrites it * left-to-right). * *

Used by the Step-Up phase EXCHANGE / ENVELOPE handlers for the * reader→device direction per spec §8.3.1.9. The expedited-phase * decrypt path on the reader side is not exercised by the applet; this * exists to verify the GCM tag on inbound traffic so the applet knows * the reader holds the matching session keys. * * @param key 32-byte AES-256 key * @param iv 12-byte IV * @param in input buffer, layout {@code ciphertext || tag} * @param inOff, inLen input region; {@code inLen >= 16} required * @param out output buffer, must have at least {@code inLen - 16} bytes * available at {@code outOff} * @return plaintext length = {@code inLen - 16} * @throws CryptoException with reason {@code ILLEGAL_VALUE} on bad input * length or tag mismatch */ short decrypt( byte[] key, short keyOff, byte[] iv, short ivOff, byte[] in, short inOff, short inLen, byte[] out, short outOff) { if (inLen < TAG_LEN) { CryptoException.throwIt(CryptoException.ILLEGAL_VALUE); } short ctLen = (short) (inLen - TAG_LEN); short tagOff = (short) (inOff + ctLen); setKeyAndIv(key, keyOff, iv, ivOff); // GHASH over the PROVIDED ciphertext first (so tag verify doesn't // depend on a successful decrypt). AAD is empty. Util.arrayFillNonAtomic(scratch, OFF_GHASH, BLOCK_LEN, (byte) 0); short consumed = 0; while (consumed < ctLen) { short chunk = (short) (ctLen - consumed); if (chunk >= BLOCK_LEN) { for (short i = 0; i < BLOCK_LEN; i++) { scratch[(short) (OFF_GHASH + i)] ^= in[(short) (inOff + consumed + i)]; } consumed += BLOCK_LEN; } else { for (short i = 0; i < chunk; i++) { scratch[(short) (OFF_GHASH + i)] ^= in[(short) (inOff + consumed + i)]; } consumed += chunk; } gfMul4Bit(scratch, OFF_GHASH); Util.arrayCopyNonAtomic(scratch, OFF_ECB_OUT, scratch, OFF_GHASH, BLOCK_LEN); } // len_block = 0^64 || (8*ctLen)^64. AAD bits = 0; same byte-shift // dance as encrypt() to dodge JC's int-promotion conversion failure. Util.arrayFillNonAtomic(scratch, OFF_LEN_BLK, BLOCK_LEN, (byte) 0); short ctBitsLo = (short) (ctLen << 3); short ctBitsHi = (short) (((short)(ctLen >>> 13)) & 0x07); scratch[(short) (OFF_LEN_BLK + 13)] = (byte) (ctBitsHi & 0xFF); scratch[(short) (OFF_LEN_BLK + 14)] = (byte) ((ctBitsLo >>> 8) & 0xFF); scratch[(short) (OFF_LEN_BLK + 15)] = (byte) (ctBitsLo & 0xFF); for (short i = 0; i < BLOCK_LEN; i++) { scratch[(short) (OFF_GHASH + i)] ^= scratch[(short) (OFF_LEN_BLK + i)]; } gfMul(scratch, OFF_GHASH, scratch, OFF_H); // T_expected = AES_K(J0) XOR GHASH. Compare against received tag in // constant-ish time (XOR-then-OR; no early exit). On JC this isn't // truly constant-time at the bytecode level, but the smartcard SE // doesn't expose timing channels at the resolution that would matter // for a 128-bit tag forgery anyway. aesEcb.doFinal(scratch, OFF_J0, BLOCK_LEN, scratch, OFF_ECB_OUT); byte diff = 0; for (short i = 0; i < TAG_LEN; i++) { byte expected = (byte) (scratch[(short) (OFF_ECB_OUT + i)] ^ scratch[(short) (OFF_GHASH + i)]); diff |= (byte) (expected ^ in[(short) (tagOff + i)]); } if (diff != 0) { // Wipe scratch before throwing so a tag-mismatch doesn't leave H, // GHASH state, or AES_K(J0) sitting in transient. Util.arrayFillNonAtomic(scratch, (short) 0, SCRATCH_LEN, (byte) 0); CryptoException.throwIt(CryptoException.ILLEGAL_VALUE); } // Tag verified -- now GCTR-decrypt the ciphertext into out[]. cb is // INC32(J0) just like encrypt(). Reusable OFF_CB slot has been free // since J0 was last referenced for the tag XOR. Util.arrayCopyNonAtomic(scratch, OFF_J0, scratch, OFF_CB, BLOCK_LEN); inc32(scratch, OFF_CB); if (usesNativeCtr != 0) { aesCtr.init(aesKey, Cipher.MODE_ENCRYPT, scratch, OFF_CB, BLOCK_LEN); // CTR is symmetric: encrypting the ciphertext with the same // keystream produces the plaintext. aesCtr.doFinal(in, inOff, ctLen, out, outOff); } else { short produced = 0; while (produced < ctLen) { short blockLen = (short) (ctLen - produced); if (blockLen > BLOCK_LEN) blockLen = BLOCK_LEN; aesEcb.doFinal(scratch, OFF_CB, BLOCK_LEN, scratch, OFF_ECB_OUT); for (short i = 0; i < blockLen; i++) { out[(short) (outOff + produced + i)] = (byte) (in[(short) (inOff + produced + i)] ^ scratch[(short) (OFF_ECB_OUT + i)]); } inc32(scratch, OFF_CB); produced += blockLen; } } Util.arrayFillNonAtomic(scratch, (short) 0, SCRATCH_LEN, (byte) 0); return ctLen; } /** * Per-(key, IV) GCM setup, shared by {@link #encrypt} and {@link #decrypt}: * loads the AES-256 key, derives H = AES_K(0^128), rebuilds the 4-bit * GHASH M-table, and lays down J0 = IV || 0x00000001 at OFF_J0. M2D.3's * stream-encrypt path calls {@link #encrypt} twice per Step-Up session * with a different (key, IV) each time; consolidating the rekey path * keeps that contract pinned to one method. */ private void setKeyAndIv( byte[] key, short keyOff, byte[] iv, short ivOff) { aesKey.setKey(key, keyOff); aesEcb.init(aesKey, Cipher.MODE_ENCRYPT); // H = AES_K(0^128). Zero scratch[OFF_H..OFF_H+16) and encrypt in place. Util.arrayFillNonAtomic(scratch, OFF_H, BLOCK_LEN, (byte) 0); aesEcb.doFinal(scratch, OFF_H, BLOCK_LEN, scratch, OFF_H); // Build the 4-bit GHASH M-table from the fresh H. One-time cost per // encrypt (~700 JC bytecodes), amortized over the ~10 gfMul4Bit calls. buildMTable(); // J0 = IV || 0x00000001 (12B IV + 4B counter). 96-bit IV canonical case. Util.arrayCopyNonAtomic(iv, ivOff, scratch, OFF_J0, IV_LEN); scratch[(short) (OFF_J0 + 12)] = 0x00; scratch[(short) (OFF_J0 + 13)] = 0x00; scratch[(short) (OFF_J0 + 14)] = 0x00; scratch[(short) (OFF_J0 + 15)] = 0x01; } /** * INC32 per NIST SP 800-38D §6.2: increments the last 4 bytes of the * 16-byte block, big-endian, modulo 2^32. */ /** * Builds the 16-entry M-table from H in scratch[OFF_H..OFF_H+16). Each * entry M[i] = i_poly · H where i_poly is the polynomial whose bit-j * coefficient is (i >> (3-j)) & 1, i.e. MSB of i (bit 3) is the α^0 term. * *

Iterative construction needing 3 shifts + 11 XORs: * M[8] = H, M[4] = M[8] >> 1, M[2] = M[4] >> 1, M[1] = M[2] >> 1 * M[i] for non-power-of-two i = XOR of M[2^k] for each set bit k. */ private void buildMTable() { // M[0] = 0 Util.arrayFillNonAtomic(mTable, (short) 0, BLOCK_LEN, (byte) 0); // M[8] = H Util.arrayCopyNonAtomic(scratch, OFF_H, mTable, (short) (8 * BLOCK_LEN), BLOCK_LEN); // M[4] = M[8] >> 1 shiftRight1WithR(mTable, (short) (8 * BLOCK_LEN), mTable, (short) (4 * BLOCK_LEN)); // M[2] = M[4] >> 1 shiftRight1WithR(mTable, (short) (4 * BLOCK_LEN), mTable, (short) (2 * BLOCK_LEN)); // M[1] = M[2] >> 1 shiftRight1WithR(mTable, (short) (2 * BLOCK_LEN), mTable, (short) (1 * BLOCK_LEN)); // M[3, 5, 6, 7, 9..15] = XOR combinations of M[1], M[2], M[4], M[8]. // Inner loop unrolled to dodge JC's "int promotion" rejection on // expressions like `1 << bit` and `(1 << bit) * BLOCK_LEN`. for (short i = 3; i < 16; i++) { if (i == 4 || i == 8) continue; // pure powers already built short destOff = (short) (i * BLOCK_LEN); Util.arrayFillNonAtomic(mTable, destOff, BLOCK_LEN, (byte) 0); // bit 0 set -> XOR M[1] if ((i & 0x01) != 0) { short srcOff = (short) (1 * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { mTable[(short) (destOff + k)] ^= mTable[(short) (srcOff + k)]; } } // bit 1 set -> XOR M[2] if ((i & 0x02) != 0) { short srcOff = (short) (2 * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { mTable[(short) (destOff + k)] ^= mTable[(short) (srcOff + k)]; } } // bit 2 set -> XOR M[4] if ((i & 0x04) != 0) { short srcOff = (short) (4 * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { mTable[(short) (destOff + k)] ^= mTable[(short) (srcOff + k)]; } } // bit 3 set -> XOR M[8] if ((i & 0x08) != 0) { short srcOff = (short) (8 * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { mTable[(short) (destOff + k)] ^= mTable[(short) (srcOff + k)]; } } } } /** * Shifts 16 bytes right by 1 bit (toward higher indices in NIST-reflected * representation) and applies the GHASH reduction polynomial R = * 0xE1 || 0^120 at byte 0 if the dropped LSB was set. Used only by * {@link #buildMTable}; the per-multiply path uses shiftRight4WithL. */ private static void shiftRight1WithR( byte[] srcBuf, short srcOff, byte[] dstBuf, short dstOff) { boolean lsb = (srcBuf[(short) (srcOff + 15)] & 0x01) != 0; short carry = 0; for (short k = 0; k < BLOCK_LEN; k++) { short cur = (short) (srcBuf[(short) (srcOff + k)] & 0xFF); short newCur = (short) ((cur >>> 1) | carry); carry = (short) ((cur & 0x01) << 7); dstBuf[(short) (dstOff + k)] = (byte) newCur; } if (lsb) { dstBuf[dstOff] ^= (byte) 0xE1; } } /** * In-place shift of scratch[OFF_ECB_OUT..OFF_ECB_OUT+16) right by 4 bits, * applying the L-table reduction for whatever 4 bits dropped off the LSB * end. This is the per-multiply hot loop's shift primitive, ~3-4× faster * than four consecutive shiftRight1WithR calls because the byte shift is * one pass and the reduction is two byte XORs from precomputed L tables. */ private void shiftRight4WithL() { // The 4 bits about to be dropped = low nibble of byte 15. short droppedNibble = (short) (scratch[(short) (OFF_ECB_OUT + 15)] & 0x0F); // Shift bytes right by 4 bits: byte k becomes (byte_{k-1} << 4) | (byte_k >> 4). // Walk high to low so we don't read overwritten data. for (short k = 15; k > 0; k--) { short cur = (short) (scratch[(short) (OFF_ECB_OUT + k)] & 0xFF); short prev = (short) (scratch[(short) (OFF_ECB_OUT + k - 1)] & 0xFF); scratch[(short) (OFF_ECB_OUT + k)] = (byte) ((cur >>> 4) | ((prev & 0x0F) << 4)); } short cur0 = (short) (scratch[OFF_ECB_OUT] & 0xFF); scratch[OFF_ECB_OUT] = (byte) (cur0 >>> 4); // Apply reduction via L-table: bytes 2..15 of L[n] are always 0 for // the NIST GHASH polynomial, so we only XOR bytes 0 and 1. scratch[OFF_ECB_OUT] ^= L_BYTE0[droppedNibble]; scratch[(short) (OFF_ECB_OUT + 1)] ^= L_BYTE1[droppedNibble]; } /** * 4-bit GHASH multiply: Z = X · H using the precomputed {@link #mTable}. * Reads X from {@code xBuf[xOff..xOff+16)} and writes the result to * scratch[OFF_ECB_OUT..OFF_ECB_OUT+16). Caller is responsible for copying * the result back to OFF_GHASH between multiplies (mirrors the existing * {@link #gfMul} contract). * *

Algorithm (LSB-first Horner): *

     *   Z = 0
     *   for each nibble of X, LSB-first:
     *       Z = (Z >> 4 with L reduction) XOR M[nibble]
     * 
* 32 iterations vs the bit-by-bit form's 128, plus the inner shift is * ~4× cheaper. Net ~3-4× speedup on the J3R-family bytecode interpreter. */ private void gfMul4Bit(byte[] xBuf, short xOff) { Util.arrayFillNonAtomic(scratch, OFF_ECB_OUT, BLOCK_LEN, (byte) 0); for (short byteIdx = 15; byteIdx >= 0; byteIdx--) { short xByte = (short) (xBuf[(short) (xOff + byteIdx)] & 0xFF); // Low nibble first (LSB of byte, processed before high nibble). short nLo = (short) (xByte & 0x0F); shiftRight4WithL(); if (nLo != 0) { short mOff = (short) (nLo * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { scratch[(short) (OFF_ECB_OUT + k)] ^= mTable[(short) (mOff + k)]; } } // High nibble. short nHi = (short) ((xByte >>> 4) & 0x0F); shiftRight4WithL(); if (nHi != 0) { short mOff = (short) (nHi * BLOCK_LEN); for (short k = 0; k < BLOCK_LEN; k++) { scratch[(short) (OFF_ECB_OUT + k)] ^= mTable[(short) (mOff + k)]; } } } } private static void inc32(byte[] buf, short off) { short i = (short) (off + 15); // Carry propagation across 4 bytes. if ((byte) (++buf[i]) != (byte) 0) return; i--; if ((byte) (++buf[i]) != (byte) 0) return; i--; if ((byte) (++buf[i]) != (byte) 0) return; i--; buf[i]++; } /** * In-place GF(2^128) multiplication per NIST SP 800-38D §6.3, storing * X ·_GF Y into X. Uses the bit-reflected convention: * * *

Algorithm 1 from NIST SP 800-38D: *

     *   Z = 0; V = Y
     *   for i = 0..127:
     *     if bit i of X is 1: Z ^= V
     *     if lowest bit of V (byte 15 bit 0) is 1: V = (V >> 1) ^ R
     *     else: V = V >> 1
     *   return Z
     * 
* *

This is the simple, portable NIST shift-XOR form. A 4-bit-table * variant would be ~4x faster but adds 32 bytes of per-H precomputation * and a more intricate shift; we encrypt ~137B per AUTH1, so the extra * cycles are acceptable versus the complexity cost. */ private void gfMul(byte[] xBuf, short xOff, byte[] yBuf, short yOff) { // V = Y (copied into OFF_TMP scratch region; gets shifted in place) Util.arrayCopyNonAtomic(yBuf, yOff, scratch, OFF_TMP, BLOCK_LEN); // Z = 0 (we'll accumulate in a second 16-byte region — reuse OFF_LEN_BLK, // which is free until after the main GHASH loop finishes). gfMul is // called both during GHASH accumulation AND during final-length mixing; // at final-length mixing OFF_LEN_BLK has already been consumed, so it's // safe to overwrite. But to avoid an aliasing bug if OFF_LEN_BLK is // ever reused differently, we stage Z in OFF_ECB_OUT instead. // // Wait — OFF_ECB_OUT holds the AES keystream we need later for the // tag (T = AES_K(J0) XOR GHASH). However, gfMul is never called // between producing OFF_ECB_OUT (the AES_K(J0) output) and consuming // it for the tag XOR. The last gfMul is for (state ^ len_block) ·_GF H, // and only AFTER that do we compute AES_K(J0) into OFF_ECB_OUT. So // using OFF_ECB_OUT as the Z accumulator is safe. Util.arrayFillNonAtomic(scratch, OFF_ECB_OUT, BLOCK_LEN, (byte) 0); for (short byteIdx = 0; byteIdx < BLOCK_LEN; byteIdx++) { byte xByte = xBuf[(short) (xOff + byteIdx)]; for (short bit = 7; bit >= 0; bit--) { // bit of X at polynomial position (byteIdx*8 + (7-bit)): byte-high bit first. if (((xByte >> bit) & 0x01) != 0) { for (short k = 0; k < BLOCK_LEN; k++) { scratch[(short) (OFF_ECB_OUT + k)] ^= scratch[(short) (OFF_TMP + k)]; } } // V = V >> 1; if the bit that fell off (byte 15 low bit BEFORE shift) is 1, // also XOR the reduction polynomial {0xE1, 0x00, ..., 0x00}. boolean lsb = (scratch[(short) (OFF_TMP + 15)] & 0x01) != 0; // Shift V right by 1 bit (toward higher byte indices). short carry = 0; for (short k = 0; k < BLOCK_LEN; k++) { short cur = (short) (scratch[(short) (OFF_TMP + k)] & 0xFF); short newCur = (short) ((cur >>> 1) | carry); carry = (short) ((cur & 0x01) << 7); scratch[(short) (OFF_TMP + k)] = (byte) newCur; } if (lsb) { scratch[OFF_TMP] ^= (byte) 0xE1; } } } // Copy Z back into X in place. Util.arrayCopyNonAtomic(scratch, OFF_ECB_OUT, xBuf, xOff, BLOCK_LEN); } }