A MAC authenticates bytes, so two distinct field tuples that serialize to the same bytes receive the same tag under one key.
Java HMAC field framing: prevent ambiguous concatenation
Encode boundaries before signing
The pairs (AB, C) and (A, BC) both become ABC under naive concatenation. That is not a cryptographic collision; the application supplied identical input. Prefix each UTF-8 field with its byte length before updating the MAC so tuple boundaries survive serialization.
The fixture prints equality for both encodings. This format is only a local protocol sketch: a stored protocol also needs a version and a canonical definition for every field type. HMAC verification explains the tag's authentication role.
Validate lengths at ingestion
A length prefix does not limit the resource cost of an untrusted field. Cap each field before allocating or MACing it; reject a negative or oversized advertised length in a decoder before reading a body.
Working program
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class OrderTupleTag {
static byte[] framed(String first, String second) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes);
for (String field : new String[] { first, second }) {
byte[] encoded = field.getBytes(StandardCharsets.UTF_8);
output.writeInt(encoded.length);
output.write(encoded);
}
return bytes.toByteArray();
}
static byte[] tag(SecretKey key, byte[] message) throws Exception {
Mac signer = Mac.getInstance("HmacSHA256");
signer.init(key);
return signer.doFinal(message);
}
public static void main(String[] args) throws Exception {
SecretKey key = KeyGenerator.getInstance("HmacSHA256").generateKey();
byte[] leftRaw = tag(key, "ABC".getBytes(StandardCharsets.UTF_8));
byte[] rightRaw = tag(key, ("A" + "BC").getBytes(StandardCharsets.UTF_8));
System.out.println("raw same=" + MessageDigest.isEqual(leftRaw, rightRaw));
System.out.println("framed same=" + MessageDigest.isEqual(tag(key, framed("AB", "C")), tag(key, framed("A", "BC"))));
}
}Output
raw same=true
framed same=falseCost and ownership
Framing and MAC calculation each process the fields' bytes, so time and temporary memory grow with total encoded length. A streaming encoder can avoid the whole-message buffer when records are large; its verifier must use exactly the same field order and lengths.
Common Mistakes
- Do not concatenate variable-length fields without a boundary rule.
- Do not mistake an application serialization collision for a hash-function collision.
- Do not accept an unbounded advertised field length.
Read next
Java HmacSHA256 verification: reject a changed message, Java ByteBuffer byte order: decode the protocol before reading integers, Java DataInputStream.readFully: EOF can leave partial bytes in the buffer, Java SHA-256: hash declared bytes, not an implicit string encoding.
