refactor(applet): AliroGcm.setKeyAndIv consolidates re-keying (M2C.2)
This commit is contained in:
@@ -144,22 +144,7 @@ final class AliroGcm {
|
|||||||
byte[] pt, short ptOff, short ptLen,
|
byte[] pt, short ptOff, short ptLen,
|
||||||
byte[] out, short outOff) {
|
byte[] out, short outOff) {
|
||||||
|
|
||||||
aesKey.setKey(key, keyOff);
|
setKeyAndIv(key, keyOff, iv, ivOff);
|
||||||
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;
|
|
||||||
|
|
||||||
// cb = INC32(J0) — first counter block for the plaintext stream.
|
// cb = INC32(J0) — first counter block for the plaintext stream.
|
||||||
Util.arrayCopyNonAtomic(scratch, OFF_J0, scratch, OFF_CB, BLOCK_LEN);
|
Util.arrayCopyNonAtomic(scratch, OFF_J0, scratch, OFF_CB, BLOCK_LEN);
|
||||||
@@ -305,23 +290,7 @@ final class AliroGcm {
|
|||||||
short ctLen = (short) (inLen - TAG_LEN);
|
short ctLen = (short) (inLen - TAG_LEN);
|
||||||
short tagOff = (short) (inOff + ctLen);
|
short tagOff = (short) (inOff + ctLen);
|
||||||
|
|
||||||
aesKey.setKey(key, keyOff);
|
setKeyAndIv(key, keyOff, iv, ivOff);
|
||||||
aesEcb.init(aesKey, Cipher.MODE_ENCRYPT);
|
|
||||||
|
|
||||||
// H = AES_K(0^128). Same as encrypt() -- GCM is one-direction at the
|
|
||||||
// primitive level: encrypt and decrypt both run GCTR + GHASH and
|
|
||||||
// differ only in whether GHASH consumes provided ciphertext or
|
|
||||||
// freshly-emitted ciphertext, plus the tag compare/emit step.
|
|
||||||
Util.arrayFillNonAtomic(scratch, OFF_H, BLOCK_LEN, (byte) 0);
|
|
||||||
aesEcb.doFinal(scratch, OFF_H, BLOCK_LEN, scratch, OFF_H);
|
|
||||||
buildMTable();
|
|
||||||
|
|
||||||
// J0 = IV || 0x00000001 (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;
|
|
||||||
|
|
||||||
// GHASH over the PROVIDED ciphertext first (so tag verify doesn't
|
// GHASH over the PROVIDED ciphertext first (so tag verify doesn't
|
||||||
// depend on a successful decrypt). AAD is empty.
|
// depend on a successful decrypt). AAD is empty.
|
||||||
@@ -412,6 +381,35 @@ final class AliroGcm {
|
|||||||
return ctLen;
|
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
|
* INC32 per NIST SP 800-38D §6.2: increments the last 4 bytes of the
|
||||||
* 16-byte block, big-endian, modulo 2^32.
|
* 16-byte block, big-endian, modulo 2^32.
|
||||||
|
|||||||
@@ -191,6 +191,65 @@ class AliroGcmTest {
|
|||||||
"different keys must produce different tags");
|
"different keys must produce different tags");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rekey-across-encrypts: M2D.3's stream-encrypt path calls
|
||||||
|
* {@link AliroGcm#encrypt} twice in a single session (once for the
|
||||||
|
* EXCHANGE Reader Status ack, once for the ENVELOPE DeviceResponse) with
|
||||||
|
* a different (key, IV) pair each time. This test pins that no leftover
|
||||||
|
* state from the first call (cached H, M-table, AES key slot) corrupts
|
||||||
|
* the second. M2C.2 extracts the rekey path into {@code setKeyAndIv};
|
||||||
|
* this test is the regression spec for that refactor.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void setKeyAndIvAllowsRekeyAcrossMultipleEncrypts() {
|
||||||
|
AliroGcm gcm = new AliroGcm();
|
||||||
|
|
||||||
|
// Encrypt block A under key1 + iv1
|
||||||
|
byte[] keyA = makeKey((byte) 0xA0);
|
||||||
|
byte[] ivA = makeIv((byte) 0xAA);
|
||||||
|
byte[] ptA = bytes("hello A");
|
||||||
|
byte[] outA = new byte[ptA.length + 16];
|
||||||
|
gcm.encrypt(keyA, (short) 0, ivA, (short) 0,
|
||||||
|
ptA, (short) 0, (short) ptA.length, outA, (short) 0);
|
||||||
|
|
||||||
|
// Rekey + encrypt block B under key2 + iv2
|
||||||
|
byte[] keyB = makeKey((byte) 0xB0);
|
||||||
|
byte[] ivB = makeIv((byte) 0xBB);
|
||||||
|
byte[] ptB = bytes("hello B");
|
||||||
|
byte[] outB = new byte[ptB.length + 16];
|
||||||
|
gcm.encrypt(keyB, (short) 0, ivB, (short) 0,
|
||||||
|
ptB, (short) 0, (short) ptB.length, outB, (short) 0);
|
||||||
|
|
||||||
|
// Both must round-trip cleanly through decrypt under their own key+iv
|
||||||
|
byte[] ptAroundTrip = new byte[ptA.length];
|
||||||
|
gcm.decrypt(keyA, (short) 0, ivA, (short) 0,
|
||||||
|
outA, (short) 0, (short) outA.length, ptAroundTrip, (short) 0);
|
||||||
|
assertArrayEquals(ptA, ptAroundTrip,
|
||||||
|
"block A must decrypt under (keyA, ivA) after gcm has been re-keyed to B");
|
||||||
|
|
||||||
|
byte[] ptBroundTrip = new byte[ptB.length];
|
||||||
|
gcm.decrypt(keyB, (short) 0, ivB, (short) 0,
|
||||||
|
outB, (short) 0, (short) outB.length, ptBroundTrip, (short) 0);
|
||||||
|
assertArrayEquals(ptB, ptBroundTrip,
|
||||||
|
"block B must decrypt under (keyB, ivB) — no leftover state from A");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] makeKey(byte seed) {
|
||||||
|
byte[] k = new byte[32];
|
||||||
|
for (int i = 0; i < 32; i++) k[i] = (byte) (seed + i);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] makeIv(byte seed) {
|
||||||
|
byte[] iv = new byte[12];
|
||||||
|
for (int i = 0; i < 12; i++) iv[i] = (byte) (seed + i);
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] bytes(String s) {
|
||||||
|
return s.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void differentIvsProduceDifferentCiphertexts() {
|
void differentIvsProduceDifferentCiphertexts() {
|
||||||
byte[] key = new byte[32];
|
byte[] key = new byte[32];
|
||||||
|
|||||||
Reference in New Issue
Block a user