diff --git a/aaa4j-radius-core/src/main/java/org/aaa4j/radius/core/packet/PacketCodec.java b/aaa4j-radius-core/src/main/java/org/aaa4j/radius/core/packet/PacketCodec.java index 73ef4de..a4ee5f6 100644 --- a/aaa4j-radius-core/src/main/java/org/aaa4j/radius/core/packet/PacketCodec.java +++ b/aaa4j-radius-core/src/main/java/org/aaa4j/radius/core/packet/PacketCodec.java @@ -45,6 +45,8 @@ */ public final class PacketCodec { + private static final int ACCOUNTING_REQUEST_CODE = 4; + private static final int MESSAGE_AUTHENTICATOR_TYPE = 80; private final Dictionary dictionary; @@ -74,7 +76,7 @@ public PacketCodec(Dictionary dictionary, RandomProvider randomProvider) { /** * Constructs a packet codec with the given dictionary, random provider, and packet identifier generator. - * + * * @param dictionary the dictionary to use * @param randomProvider the random provider to use * @param packetIdGenerator the packet identifier generator to use @@ -110,13 +112,23 @@ private static Mac getHmacMd5Instance() { * * @param request the request packet to encode * @param secret the shared secret - * + * @param requestAuthenticator the request authenticator (must be 16 bytes in length); populated by this method for + * Accounting-Request packets + * * @return byte array of the encoded request packet - * + * * @throws PacketCodecException if there's a problem encoding the packet */ public byte[] encodeRequest(Packet request, byte[] secret, byte[] requestAuthenticator) throws PacketCodecException { + if (requestAuthenticator.length != 16) { + throw new IllegalArgumentException("requestAuthenticator length must be 16"); + } + + if (request.getCode() == ACCOUNTING_REQUEST_CODE) { + Arrays.fill(requestAuthenticator, (byte) 0x00); + } + CodecContext codecContext = new CodecContext(dictionary, secret, requestAuthenticator, randomProvider); List rawAttributes = encodeAttributes(codecContext, request.getAttributes()); @@ -177,6 +189,16 @@ public byte[] encodeRequest(Packet request, byte[] secret, byte[] requestAuthent System.arraycopy(messageAuthenticator, 0, bytes, messageAuthenticatorPosition + 2, 16); } + if (request.getCode() == ACCOUNTING_REQUEST_CODE) { + MessageDigest md5 = getMd5Instance(); + md5.update(bytes); + md5.update(secret); + byte[] accountingRequestAuthenticator = md5.digest(); + + System.arraycopy(accountingRequestAuthenticator, 0, bytes, 4, 16); + System.arraycopy(accountingRequestAuthenticator, 0, requestAuthenticator, 0, 16); + } + return bytes; } @@ -187,13 +209,17 @@ public byte[] encodeRequest(Packet request, byte[] secret, byte[] requestAuthent * @param secret the shared secret * @param requestId the request identifier * @param requestAuthenticator the request authenticator - * + * * @return byte array of the encoded response packet - * + * * @throws PacketCodecException if there's a problem encoding the packet */ public byte[] encodeResponse(Packet response, byte[] secret, int requestId, byte[] requestAuthenticator) throws PacketCodecException { + if (requestAuthenticator.length != 16) { + throw new IllegalArgumentException("requestAuthenticator length must be 16"); + } + CodecContext codecContext = new CodecContext(dictionary, secret, requestAuthenticator, randomProvider); List rawAttributes = encodeAttributes(codecContext, response.getAttributes()); @@ -269,9 +295,9 @@ public byte[] encodeResponse(Packet response, byte[] secret, int requestId, byte * * @param bytes byte array of the encoded request packet * @param secret the shared secret - * + * * @return a packet object - * + * * @throws PacketCodecException if the packet is malformed or there's a problem decoding the request packet */ public Packet decodeRequest(byte[] bytes, byte[] secret) throws PacketCodecException { @@ -291,6 +317,19 @@ public Packet decodeRequest(byte[] bytes, byte[] secret) throws PacketCodecExcep byte[] authenticatorBytes = new byte[16]; System.arraycopy(bytes, 4, authenticatorBytes, 0, 16); + if (code == ACCOUNTING_REQUEST_CODE) { + Arrays.fill(bytes, 4, 4 + 16, (byte) 0x00); + + MessageDigest md5 = getMd5Instance(); + md5.update(bytes); + md5.update(secret); + byte[] calculatedAccountingRequestAuthenticator = md5.digest(); + + if (!Arrays.equals(authenticatorBytes, calculatedAccountingRequestAuthenticator)) { + throw new PacketCodecException("Invalid Accounting-Request packet authenticator"); + } + } + CodecContext codecContext = new CodecContext(dictionary, secret, authenticatorBytes, randomProvider); List rawAttributes = new ArrayList<>(); @@ -372,9 +411,9 @@ public Packet decodeRequest(byte[] bytes, byte[] secret) throws PacketCodecExcep * @param bytes byte array of the encoded request packet * @param secret the shared secret * @param requestAuthenticator the request authenticator - * + * * @return a packet object - * + * * @throws PacketCodecException if the packet is malformed or there's a problem decoding the request packet */ public Packet decodeResponse(byte[] bytes, byte[] secret, byte[] requestAuthenticator) throws PacketCodecException { diff --git a/aaa4j-radius-core/src/test/java/org/aaa4j/radius/core/packet/PacketCodecTest.java b/aaa4j-radius-core/src/test/java/org/aaa4j/radius/core/packet/PacketCodecTest.java index 646437b..62c9fc0 100644 --- a/aaa4j-radius-core/src/test/java/org/aaa4j/radius/core/packet/PacketCodecTest.java +++ b/aaa4j-radius-core/src/test/java/org/aaa4j/radius/core/packet/PacketCodecTest.java @@ -163,6 +163,67 @@ void decodeResponse() throws PacketCodecException { } + @Nested + @DisplayName("Accounting packet encoding and decoding") + class AccountingPackets { + + @Test + @DisplayName("Accounting-Request packet is encoded successfully") + void encodeRequest() throws PacketCodecException { + when(mockedPacketIdGenerator.nextId()).thenReturn(42); + + Packet requestPacket = new Packet(4, List.of( + new RawAttribute(32, fromHex("5353494431")), + new RawAttribute(40, fromHex("00000001")), + new RawAttribute(44, fromHex("616263313233")))); + + byte[] accountingRequestAuthenticator = new byte[16]; + + byte[] actual = packetCodec.encodeRequest(requestPacket, "abc123".getBytes(US_ASCII), + accountingRequestAuthenticator); + + assertEquals("042a00295129abcd7a107c329bee6866d2887782200753534944312806000000" + + "012c08616263313233", + toHex(actual)); + + assertEquals("5129abcd7a107c329bee6866d2887782", + toHex(accountingRequestAuthenticator)); + } + + @Test + @DisplayName("Accounting-Request packet is decoded successfully") + void decodeRequest() throws PacketCodecException { + byte[] encoded = fromHex("042a00295129abcd7a107c329bee6866d2887782200753534944312806000000" + + "012c08616263313233"); + + Packet requestPacket = packetCodec.decodeRequest(encoded, "abc123".getBytes(US_ASCII)); + + assertEquals(4, requestPacket.getCode()); + assertEquals(42, requestPacket.getReceivedFields().getIdentifier()); + assertEquals("5129abcd7a107c329bee6866d2887782", + toHex(requestPacket.getReceivedFields().getAuthenticator())); + assertEquals(3, requestPacket.getAttributes().size()); + assertEquals(new RawAttribute(32, fromHex("5353494431")), + requestPacket.getAttributes().get(0)); + assertEquals(new RawAttribute(40, fromHex("00000001")), + requestPacket.getAttributes().get(1)); + assertEquals(new RawAttribute(44, fromHex("616263313233")), + requestPacket.getAttributes().get(2)); + } + + @Test + @DisplayName("Accounting-Request packet with invalid request authenticator throws codec exception") + void decodeRequestWithInvalidRequestAuthenticatorThrows() throws PacketCodecException { + byte[] encoded = fromHex("042a0029ffffabcd7a107c329bee6866d2887782200753534944312806000000" + + "012c08616263313233"); + + assertThrows(PacketCodecException.class, () -> { + packetCodec.decodeRequest(encoded, "abc123".getBytes(US_ASCII)); + }); + } + + } + @Nested @DisplayName("Malformed packets") class MalformedPackets {