diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 0cdcad4c..320a20d2 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -16,81 +16,146 @@ * Annotation interface for marking and categorizing modifiable variables within a class. * *

This annotation provides metadata about a modifiable variable field, including its semantic - * type (what kind of data it represents) and format (how the data is encoded). This information can - * be used for reflection-based analysis, serialization, or other operations that need to understand - * the purpose of different variables. - * - *

//TODO This class has not been touched or used much for a while and needs refactoring. + * type (what kind of data it represents) and format (how the data is encoded). This information is + * used for reflection-based analysis, serialization, and testing scenarios. * *

The annotation is retained at runtime and can only be applied to fields. + * + *

Usage examples: + * + *

{@code
+ * // Variable length field
+ * @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4)
+ * private ModifiableInteger messageLength;
+ *
+ * // Signature with specific encoding and length
+ * @ModifiableVariableProperty(
+ *     purpose = Purpose.SIGNATURE,
+ *     encoding = Encoding.ASN1_DER,
+ *     minLength = 70,
+ *     maxLength = 73)
+ * private ModifiableByteArray digitalSignature;
+ *
+ * // Random value
+ * @ModifiableVariableProperty(purpose = Purpose.RANDOM)
+ * private ModifiableByteArray nonce;
+ *
+ * // Variable-length data with description
+ * @ModifiableVariableProperty(
+ *     purpose = Purpose.PLAINTEXT,
+ *     encoding = Encoding.UTF8,
+ *     maxLength = 1024)
+ * private ModifiableByteArray applicationData;
+ * }
*/ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ModifiableVariableProperty { /** - * Semantic types that can be assigned to modifiable variables. These values describe the - * purpose or role of the variable. + * Semantic purpose categories for modifiable variables. These describe the role or function of + * the variable within any protocol or data structure. */ - enum Type { + enum Purpose { /** Variable representing a length field */ LENGTH, - /** Variable representing a count field */ + /** Variable representing a count or quantity field */ COUNT, - /** Variable representing padding */ + /** Variable representing padding or filler bytes */ PADDING, - /** Variable representing one or more (array of) TLS constants */ - TLS_CONSTANT, + /** Variable representing a protocol constant or enumerated value */ + CONSTANT, /** Variable representing a cryptographic signature */ SIGNATURE, - /** Variable representing encrypted data */ + /** Variable representing encrypted or ciphered data */ CIPHERTEXT, - /** Variable representing a message authentication code */ - HMAC, - /** Variable representing a public key */ - PUBLIC_KEY, - /** Variable representing a private key */ - PRIVATE_KEY, - /** Variable representing key material */ + /** Variable representing a message authentication code or hash */ + MAC, + /** Variable representing cryptographic key material */ KEY_MATERIAL, /** Variable representing a certificate */ CERTIFICATE, - /** Variable representing a plain protocol message, always in a decrypted state */ - PLAIN_PROTOCOL_MESSAGE, - /** Variable representing a plain record */ - PLAIN_RECORD, - /** Variable representing a cookie */ - COOKIE, - /** Default type when no specific type is applicable */ - NONE, - /** Variable that switches behavior */ - BEHAVIOR_SWITCH + /** Variable representing plaintext protocol data */ + PLAINTEXT, + /** Variable representing a random value, nonce, or salt */ + RANDOM, + /** Variable representing a session or connection identifier */ + IDENTIFIER, + /** Variable representing a timestamp or temporal value */ + TIMESTAMP, + /** Default purpose when no specific category applies */ + UNSPECIFIED } /** - * Encoding formats that can be used for modifiable variables. These values describe how the - * data is encoded. + * Encoding formats for modifiable variables. These describe how the data is encoded, + * structured, or represented. */ - enum Format { - /** ASN.1 encoding format */ - ASN1, - /** PKCS#1 encoding format */ + enum Encoding { + /** ASN.1 Distinguished Encoding Rules format */ + ASN1_DER, + /** ASN.1 Basic Encoding Rules format */ + ASN1_BER, + /** PKCS#1 format for RSA key encoding */ PKCS1, - /** Default format when no specific format is applicable */ - NONE + /** PKCS#8 format for private key information */ + PKCS8, + /** X.509 format for certificates */ + X509, + /** PEM (Privacy-Enhanced Mail) text format */ + PEM, + /** Base64 text encoding */ + BASE64, + /** Hexadecimal string representation */ + HEX_STRING, + /** Raw binary data */ + BINARY, + /** UTF-8 text encoding */ + UTF8, + /** Unsigned big-endian encoding */ + UNSIGNED_BIG_ENDIAN, + /** Unsigned little-endian encoding */ + UNSIGNED_LITTLE_ENDIAN, + /** Signed big-endian encoding */ + SIGNED_BIG_ENDIAN, + /** Signed little-endian encoding */ + SIGNED_LITTLE_ENDIAN, + + /** JSON text format */ + JSON, + /** XML text format */ + XML, + /** Default encoding when not specified */ + UNSPECIFIED } /** - * Specifies the semantic type of the annotated variable. + * Specifies the semantic purpose of the annotated variable. * - * @return The type of the variable + * @return The purpose category of the variable */ - Type type() default Type.NONE; + Purpose purpose() default Purpose.UNSPECIFIED; /** * Specifies the encoding format of the annotated variable. * - * @return The format of the variable + * @return The encoding format of the variable + */ + Encoding encoding() default Encoding.UNSPECIFIED; + + /** + * Specifies the minimum length (in bytes) of the variable's value. Use -1 to indicate no + * minimum constraint. + * + * @return The minimum length in bytes, or -1 if unconstrained + */ + int minLength() default -1; + + /** + * Specifies the maximum length (in bytes) of the variable's value. Use -1 to indicate no + * maximum constraint. + * + * @return The maximum length in bytes, or -1 if unconstrained */ - Format format() default Format.NONE; + int maxLength() default -1; } diff --git a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java index 326c129f..03d4f9ff 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java +++ b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java @@ -9,9 +9,12 @@ import de.rub.nds.modifiablevariable.HoldsModifiableVariable; import de.rub.nds.modifiablevariable.ModifiableVariable; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Encoding; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Purpose; import java.lang.reflect.Field; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +26,10 @@ * locate fields that are either of type ModifiableVariable or are annotated with {@link * HoldsModifiableVariable}. * + *

Additionally, this class provides specialized methods for working with the {@link + * ModifiableVariableProperty} annotation to categorize and analyze fields by their semantic + * meaning, encoding format, and protocol context. + * *

This class cannot be instantiated and all methods are static. */ public final class ModifiableVariableAnalyzer { @@ -205,4 +212,125 @@ public static List getAllModifiableVariableHolders } return result; } + + /** + * Retrieves all fields in a class hierarchy that are annotated with ModifiableVariableProperty. + * + * @param clazz The class to analyze + * @return A list of fields annotated with ModifiableVariableProperty + */ + public static List getAnnotatedFields(Class clazz) { + List annotatedFields = new ArrayList<>(); + Class currentClass = clazz; + + while (currentClass != null && currentClass != Object.class) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(ModifiableVariableProperty.class)) { + annotatedFields.add(field); + } + } + currentClass = currentClass.getSuperclass(); + } + + return annotatedFields; + } + + /** + * Groups annotated fields by their semantic purpose. + * + * @param clazz The class to analyze + * @return A map where keys are Purpose enum values and values are lists of fields + */ + public static Map> groupFieldsByPurpose(Class clazz) { + return getAnnotatedFields(clazz).stream() + .collect( + Collectors.groupingBy( + field -> + field.getAnnotation(ModifiableVariableProperty.class) + .purpose())); + } + + /** + * Groups annotated fields by their encoding format. + * + * @param clazz The class to analyze + * @return A map where keys are Encoding enum values and values are lists of fields + */ + public static Map> groupFieldsByEncoding(Class clazz) { + return getAnnotatedFields(clazz).stream() + .collect( + Collectors.groupingBy( + field -> + field.getAnnotation(ModifiableVariableProperty.class) + .encoding())); + } + + /** + * Finds all fields of a specific semantic purpose. + * + * @param clazz The class to analyze + * @param purpose The semantic purpose to search for + * @return A list of fields with the specified purpose + */ + public static List getFieldsByPurpose(Class clazz, Purpose purpose) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> + field.getAnnotation(ModifiableVariableProperty.class).purpose() + == purpose) + .collect(Collectors.toList()); + } + + /** + * Finds all fields with a specific encoding format. + * + * @param clazz The class to analyze + * @param encoding The encoding format to search for + * @return A list of fields with the specified encoding + */ + public static List getFieldsByEncoding(Class clazz, Encoding encoding) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> + field.getAnnotation(ModifiableVariableProperty.class).encoding() + == encoding) + .collect(Collectors.toList()); + } + + /** + * Checks if a field has a ModifiableVariableProperty annotation. + * + * @param field The field to check + * @return true if the field is annotated, false otherwise + */ + public static boolean isAnnotated(Field field) { + return field.isAnnotationPresent(ModifiableVariableProperty.class); + } + + /** + * Gets the ModifiableVariableProperty annotation from a field. + * + * @param field The field to examine + * @return The annotation or null if not present + */ + public static ModifiableVariableProperty getAnnotation(Field field) { + return field.getAnnotation(ModifiableVariableProperty.class); + } + + /** + * Validates that all ModifiableVariable fields in a class have proper annotations. This is + * useful for ensuring coding standards compliance. + * + * @param clazz The class to validate + * @return A list of field names that are ModifiableVariable but lack annotations + */ + public static List getUnannotatedModifiableVariables(Class clazz) { + List allModifiableFields = + ReflectionHelper.getFieldsUpTo(clazz, null, ModifiableVariable.class); + + return allModifiableFields.stream() + .filter(field -> !isAnnotated(field)) + .map(Field::getName) + .collect(Collectors.toList()); + } } diff --git a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java index 058d28e4..cedc2a3a 100644 --- a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java +++ b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java @@ -7,29 +7,50 @@ */ package de.rub.nds.modifiablevariable; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; -import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Format; -import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Type; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Encoding; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Purpose; import de.rub.nds.modifiablevariable.integer.ModifiableInteger; +import de.rub.nds.modifiablevariable.util.ModifiableVariableAnalyzer; import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; public class ModifiableVariablePropertyTest { // Test class with annotated fields private static class TestClass { - @ModifiableVariableProperty(type = Type.LENGTH) + @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4) private ModifiableInteger length; - @ModifiableVariableProperty(type = Type.COUNT) + @ModifiableVariableProperty(purpose = Purpose.COUNT) private ModifiableInteger count; - @ModifiableVariableProperty(type = Type.SIGNATURE, format = Format.ASN1) - private ModifiableInteger signatureWithFormat; + @ModifiableVariableProperty(purpose = Purpose.SIGNATURE, encoding = Encoding.ASN1_DER) + private ModifiableInteger signatureWithEncoding; @ModifiableVariableProperty private ModifiableInteger defaultProperty; + + @ModifiableVariableProperty( + purpose = Purpose.KEY_MATERIAL, + encoding = Encoding.X509, + minLength = 32, + maxLength = 65) + private ModifiableInteger enhancedProperty; + + @ModifiableVariableProperty(purpose = Purpose.CONSTANT) + private ModifiableInteger protocolVersion; + + @ModifiableVariableProperty(purpose = Purpose.RANDOM) + private ModifiableInteger randomValue; + + @ModifiableVariableProperty(purpose = Purpose.PADDING, minLength = 0, maxLength = 255) + private ModifiableInteger paddingLength; + + // Unannotated ModifiableVariable field for testing + private ModifiableInteger unannotatedField; } @Test @@ -39,8 +60,10 @@ public void testLengthProperty() throws NoSuchFieldException { lengthField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.LENGTH, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.LENGTH, annotation.purpose()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); + assertEquals(1, annotation.minLength()); + assertEquals(4, annotation.maxLength()); } @Test @@ -50,19 +73,21 @@ public void testCountProperty() throws NoSuchFieldException { countField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.COUNT, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.COUNT, annotation.purpose()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); + assertEquals(-1, annotation.minLength()); + assertEquals(-1, annotation.maxLength()); } @Test - public void testSignatureWithFormatProperty() throws NoSuchFieldException { - Field signatureField = TestClass.class.getDeclaredField("signatureWithFormat"); + public void testSignatureWithEncodingProperty() throws NoSuchFieldException { + Field signatureField = TestClass.class.getDeclaredField("signatureWithEncoding"); ModifiableVariableProperty annotation = signatureField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.SIGNATURE, annotation.type()); - assertEquals(Format.ASN1, annotation.format()); + assertEquals(Purpose.SIGNATURE, annotation.purpose()); + assertEquals(Encoding.ASN1_DER, annotation.encoding()); } @Test @@ -72,7 +97,78 @@ public void testDefaultProperty() throws NoSuchFieldException { defaultField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.NONE, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.UNSPECIFIED, annotation.purpose()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); + assertEquals(-1, annotation.minLength()); + assertEquals(-1, annotation.maxLength()); + } + + @Test + public void testEnhancedProperty() throws NoSuchFieldException { + Field enhancedField = TestClass.class.getDeclaredField("enhancedProperty"); + ModifiableVariableProperty annotation = + enhancedField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(annotation); + assertEquals(Purpose.KEY_MATERIAL, annotation.purpose()); + assertEquals(Encoding.X509, annotation.encoding()); + assertEquals(32, annotation.minLength()); + assertEquals(65, annotation.maxLength()); + } + + @Test + public void testNewPurposeEnums() throws NoSuchFieldException { + Field versionField = TestClass.class.getDeclaredField("protocolVersion"); + ModifiableVariableProperty versionAnnotation = + versionField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(versionAnnotation); + assertEquals(Purpose.CONSTANT, versionAnnotation.purpose()); + + Field randomField = TestClass.class.getDeclaredField("randomValue"); + ModifiableVariableProperty randomAnnotation = + randomField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(randomAnnotation); + assertEquals(Purpose.RANDOM, randomAnnotation.purpose()); + + Field paddingField = TestClass.class.getDeclaredField("paddingLength"); + ModifiableVariableProperty paddingAnnotation = + paddingField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(paddingAnnotation); + assertEquals(Purpose.PADDING, paddingAnnotation.purpose()); + assertEquals(0, paddingAnnotation.minLength()); + assertEquals(255, paddingAnnotation.maxLength()); + } + + @Test + public void testModifiableVariableAnalyzer() { + List annotatedFields = + ModifiableVariableAnalyzer.getAnnotatedFields(TestClass.class); + assertEquals(8, annotatedFields.size()); // 8 annotated fields in TestClass + + Map> byPurpose = + ModifiableVariableAnalyzer.groupFieldsByPurpose(TestClass.class); + assertTrue(byPurpose.containsKey(Purpose.LENGTH)); + assertTrue(byPurpose.containsKey(Purpose.KEY_MATERIAL)); + assertTrue(byPurpose.containsKey(Purpose.CONSTANT)); + assertTrue(byPurpose.containsKey(Purpose.RANDOM)); + assertTrue(byPurpose.containsKey(Purpose.PADDING)); + + List lengthFields = + ModifiableVariableAnalyzer.getFieldsByPurpose(TestClass.class, Purpose.LENGTH); + assertEquals(1, lengthFields.size()); + assertEquals("length", lengthFields.get(0).getName()); + + List x509Fields = + ModifiableVariableAnalyzer.getFieldsByEncoding(TestClass.class, Encoding.X509); + assertEquals(1, x509Fields.size()); + assertEquals("enhancedProperty", x509Fields.get(0).getName()); + + List unannotated = + ModifiableVariableAnalyzer.getUnannotatedModifiableVariables(TestClass.class); + assertEquals(1, unannotated.size()); + assertEquals("unannotatedField", unannotated.get(0)); } }