From 0d2713359f5276260e5cce257c8a55da4a032d2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:34:50 +0000 Subject: [PATCH 1/3] Initial plan From 9e60acbf261f7c0074894f15c306c36cb71d5909 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:44:03 +0000 Subject: [PATCH 2/3] Add KeyMetadata data class for symmetric key rotation Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../common/java/crypto/KeyMetadata.java | 258 ++++++++++++++++++ .../common/java/crypto/KeyMetadataTest.java | 146 ++++++++++ 2 files changed, 404 insertions(+) create mode 100644 common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java create mode 100644 common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java new file mode 100644 index 0000000000..36525566fe --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.crypto; + +import org.json.JSONException; +import org.json.JSONObject; + +import lombok.NonNull; + +/** + * Immutable metadata describing a symmetric encryption key version. + * + *

Used by the KeyVersionRegistry to track the lifecycle of symmetric keys and by + * encryption managers to select the active key or fall back to deprecated keys for + * decryption only.

+ */ +public final class KeyMetadata { + + /** Default symmetric encryption algorithm. */ + public static final String DEFAULT_ALGORITHM = "AES/CBC/PKCS5Padding"; + + /** Default key size in bits. */ + public static final int DEFAULT_KEY_SIZE = 256; + + private static final String FIELD_VERSION_ID = "versionId"; + private static final String FIELD_CREATED_AT_MILLIS = "createdAtMillis"; + private static final String FIELD_ALGORITHM = "algorithm"; + private static final String FIELD_KEY_SIZE = "keySize"; + private static final String FIELD_IS_DEPRECATED = "isDeprecated"; + + /** + * Key identifier, e.g. {@code "K001"}, {@code "K002"}. + */ + private final String mVersionId; + + /** + * Unix timestamp (milliseconds) at which this key was created. + */ + private final long mCreatedAtMillis; + + /** + * Encryption algorithm associated with this key (e.g. {@code "AES/CBC/PKCS5Padding"}). + */ + private final String mAlgorithm; + + /** + * Key size in bits (e.g. {@code 256}). + */ + private final int mKeySize; + + /** + * When {@code true} the key may only be used for decryption; new encryptions must use + * a non-deprecated key. + */ + private final boolean mIsDeprecated; + + private KeyMetadata(@NonNull final Builder builder) { + mVersionId = builder.mVersionId; + mCreatedAtMillis = builder.mCreatedAtMillis; + mAlgorithm = builder.mAlgorithm; + mKeySize = builder.mKeySize; + mIsDeprecated = builder.mIsDeprecated; + } + + /** + * Returns the key version identifier. + * + * @return non-null version id string. + */ + @NonNull + public String getVersionId() { + return mVersionId; + } + + /** + * Returns the Unix timestamp (milliseconds) at which this key was created. + * + * @return creation time in epoch milliseconds. + */ + public long getCreatedAtMillis() { + return mCreatedAtMillis; + } + + /** + * Returns the encryption algorithm string for this key. + * + * @return non-null algorithm string. + */ + @NonNull + public String getAlgorithm() { + return mAlgorithm; + } + + /** + * Returns the key size in bits. + * + * @return key size in bits. + */ + public int getKeySize() { + return mKeySize; + } + + /** + * Returns whether this key is deprecated. A deprecated key may only be used for + * decryption; new data must be encrypted with a non-deprecated key. + * + * @return {@code true} if the key is deprecated. + */ + public boolean isDeprecated() { + return mIsDeprecated; + } + + /** + * Serializes this instance to a JSON string. + * + * @return a JSON string representation of this {@link KeyMetadata}. + * @throws JSONException if serialization fails. + */ + @NonNull + public String toJson() throws JSONException { + final JSONObject json = new JSONObject(); + json.put(FIELD_VERSION_ID, mVersionId); + json.put(FIELD_CREATED_AT_MILLIS, mCreatedAtMillis); + json.put(FIELD_ALGORITHM, mAlgorithm); + json.put(FIELD_KEY_SIZE, mKeySize); + json.put(FIELD_IS_DEPRECATED, mIsDeprecated); + return json.toString(); + } + + /** + * Deserializes a {@link KeyMetadata} instance from a JSON string. + * + * @param json the JSON string produced by {@link #toJson()}. + * @return a reconstructed {@link KeyMetadata} instance. + * @throws JSONException if {@code json} is malformed or is missing required fields. + */ + @NonNull + public static KeyMetadata fromJson(@NonNull final String json) throws JSONException { + final JSONObject jsonObject = new JSONObject(json); + return new Builder() + .versionId(jsonObject.getString(FIELD_VERSION_ID)) + .createdAtMillis(jsonObject.getLong(FIELD_CREATED_AT_MILLIS)) + .algorithm(jsonObject.getString(FIELD_ALGORITHM)) + .keySize(jsonObject.getInt(FIELD_KEY_SIZE)) + .isDeprecated(jsonObject.getBoolean(FIELD_IS_DEPRECATED)) + .build(); + } + + /** + * Builder for constructing {@link KeyMetadata} instances. + */ + public static final class Builder { + + private String mVersionId; + private long mCreatedAtMillis; + private String mAlgorithm = DEFAULT_ALGORITHM; + private int mKeySize = DEFAULT_KEY_SIZE; + private boolean mIsDeprecated = false; + + /** + * Sets the key version identifier. + * + * @param versionId non-null version id, e.g. {@code "K001"}. + * @return this builder. + */ + @NonNull + public Builder versionId(@NonNull final String versionId) { + mVersionId = versionId; + return this; + } + + /** + * Sets the creation timestamp. + * + * @param createdAtMillis Unix timestamp in milliseconds. + * @return this builder. + */ + @NonNull + public Builder createdAtMillis(final long createdAtMillis) { + mCreatedAtMillis = createdAtMillis; + return this; + } + + /** + * Sets the encryption algorithm. Defaults to {@link #DEFAULT_ALGORITHM}. + * + * @param algorithm non-null algorithm string. + * @return this builder. + */ + @NonNull + public Builder algorithm(@NonNull final String algorithm) { + mAlgorithm = algorithm; + return this; + } + + /** + * Sets the key size in bits. Defaults to {@link #DEFAULT_KEY_SIZE}. + * + * @param keySize key size in bits; must be positive. + * @return this builder. + * @throws IllegalArgumentException if {@code keySize} is not positive. + */ + @NonNull + public Builder keySize(final int keySize) { + if (keySize <= 0) { + throw new IllegalArgumentException("keySize must be a positive value."); + } + mKeySize = keySize; + return this; + } + + /** + * Sets whether the key is deprecated. + * + * @param isDeprecated {@code true} if this key should only be used for decryption. + * @return this builder. + */ + @NonNull + public Builder isDeprecated(final boolean isDeprecated) { + mIsDeprecated = isDeprecated; + return this; + } + + /** + * Builds a new {@link KeyMetadata} instance. + * + * @return a new {@link KeyMetadata}. + * @throws IllegalStateException if {@code versionId} has not been set. + */ + @NonNull + public KeyMetadata build() { + if (mVersionId == null || mVersionId.isEmpty()) { + throw new IllegalStateException("versionId must be set before calling build()."); + } + return new KeyMetadata(this); + } + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java new file mode 100644 index 0000000000..fadb8060ef --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.crypto; + +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Test; + +public class KeyMetadataTest { + + private static final String VERSION_ID = "K001"; + private static final long CREATED_AT_MILLIS = 1_700_000_000_000L; + private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final int KEY_SIZE = 256; + + @Test + public void testBuilder_setsAllFields() { + final KeyMetadata metadata = new KeyMetadata.Builder() + .versionId(VERSION_ID) + .createdAtMillis(CREATED_AT_MILLIS) + .algorithm(ALGORITHM) + .keySize(KEY_SIZE) + .isDeprecated(false) + .build(); + + Assert.assertEquals(VERSION_ID, metadata.getVersionId()); + Assert.assertEquals(CREATED_AT_MILLIS, metadata.getCreatedAtMillis()); + Assert.assertEquals(ALGORITHM, metadata.getAlgorithm()); + Assert.assertEquals(KEY_SIZE, metadata.getKeySize()); + Assert.assertFalse(metadata.isDeprecated()); + } + + @Test + public void testBuilder_defaultValues() { + final KeyMetadata metadata = new KeyMetadata.Builder() + .versionId(VERSION_ID) + .createdAtMillis(CREATED_AT_MILLIS) + .build(); + + Assert.assertEquals(KeyMetadata.DEFAULT_ALGORITHM, metadata.getAlgorithm()); + Assert.assertEquals(KeyMetadata.DEFAULT_KEY_SIZE, metadata.getKeySize()); + Assert.assertFalse(metadata.isDeprecated()); + } + + @Test(expected = IllegalStateException.class) + public void testBuilder_throwsWhenVersionIdMissing() { + new KeyMetadata.Builder() + .createdAtMillis(CREATED_AT_MILLIS) + .build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuilder_throwsWhenVersionIdEmpty() { + new KeyMetadata.Builder() + .versionId("") + .createdAtMillis(CREATED_AT_MILLIS) + .build(); + } + + @Test + public void testToJson_producesValidJson() throws JSONException { + final KeyMetadata metadata = new KeyMetadata.Builder() + .versionId(VERSION_ID) + .createdAtMillis(CREATED_AT_MILLIS) + .algorithm(ALGORITHM) + .keySize(KEY_SIZE) + .isDeprecated(true) + .build(); + + final String json = metadata.toJson(); + Assert.assertNotNull(json); + Assert.assertTrue(json.contains(VERSION_ID)); + Assert.assertTrue(json.contains(String.valueOf(CREATED_AT_MILLIS))); + Assert.assertTrue(json.contains(ALGORITHM)); + Assert.assertTrue(json.contains(String.valueOf(KEY_SIZE))); + Assert.assertTrue(json.contains("true")); // isDeprecated + } + + @Test + public void testFromJson_reconstructsObject() throws JSONException { + final KeyMetadata original = new KeyMetadata.Builder() + .versionId(VERSION_ID) + .createdAtMillis(CREATED_AT_MILLIS) + .algorithm(ALGORITHM) + .keySize(KEY_SIZE) + .isDeprecated(false) + .build(); + + final String json = original.toJson(); + final KeyMetadata reconstructed = KeyMetadata.fromJson(json); + + Assert.assertEquals(original.getVersionId(), reconstructed.getVersionId()); + Assert.assertEquals(original.getCreatedAtMillis(), reconstructed.getCreatedAtMillis()); + Assert.assertEquals(original.getAlgorithm(), reconstructed.getAlgorithm()); + Assert.assertEquals(original.getKeySize(), reconstructed.getKeySize()); + Assert.assertEquals(original.isDeprecated(), reconstructed.isDeprecated()); + } + + @Test + public void testFromJson_reconstructsDeprecatedKey() throws JSONException { + final KeyMetadata original = new KeyMetadata.Builder() + .versionId("K002") + .createdAtMillis(CREATED_AT_MILLIS) + .isDeprecated(true) + .build(); + + final KeyMetadata reconstructed = KeyMetadata.fromJson(original.toJson()); + + Assert.assertEquals("K002", reconstructed.getVersionId()); + Assert.assertTrue(reconstructed.isDeprecated()); + } + + @Test(expected = JSONException.class) + public void testFromJson_throwsOnMalformedJson() throws JSONException { + KeyMetadata.fromJson("not valid json"); + } + + @Test(expected = IllegalArgumentException.class) + public void testBuilder_throwsOnInvalidKeySize() { + new KeyMetadata.Builder() + .versionId(VERSION_ID) + .createdAtMillis(CREATED_AT_MILLIS) + .keySize(0) + .build(); + } +} From 7e519e3aa56ed2a4ef343e295b89f2a63ce02572 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:06:37 +0000 Subject: [PATCH 3/3] Refactor KeyMetadata to use Lombok @Getter and @Builder Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../common/java/crypto/KeyMetadata.java | 209 ++++-------------- .../common/java/crypto/KeyMetadataTest.java | 53 +++-- 2 files changed, 81 insertions(+), 181 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java b/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java index 36525566fe..1c19454d06 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java +++ b/common4j/src/main/com/microsoft/identity/common/java/crypto/KeyMetadata.java @@ -25,6 +25,8 @@ import org.json.JSONException; import org.json.JSONObject; +import lombok.Builder; +import lombok.Getter; import lombok.NonNull; /** @@ -34,6 +36,8 @@ * encryption managers to select the active key or fall back to deprecated keys for * decryption only.

*/ +@Getter +@Builder public final class KeyMetadata { /** Default symmetric encryption algorithm. */ @@ -51,83 +55,53 @@ public final class KeyMetadata { /** * Key identifier, e.g. {@code "K001"}, {@code "K002"}. */ - private final String mVersionId; + private final String versionId; /** * Unix timestamp (milliseconds) at which this key was created. */ - private final long mCreatedAtMillis; + private final long createdAtMillis; /** - * Encryption algorithm associated with this key (e.g. {@code "AES/CBC/PKCS5Padding"}). + * Encryption algorithm for this key (e.g. {@code "AES/CBC/PKCS5Padding"}). + * Defaults to {@link #DEFAULT_ALGORITHM}. */ - private final String mAlgorithm; + @Builder.Default + private final String algorithm = DEFAULT_ALGORITHM; /** * Key size in bits (e.g. {@code 256}). + * Defaults to {@link #DEFAULT_KEY_SIZE}. */ - private final int mKeySize; + @Builder.Default + private final int keySize = DEFAULT_KEY_SIZE; /** * When {@code true} the key may only be used for decryption; new encryptions must use * a non-deprecated key. */ - private final boolean mIsDeprecated; - - private KeyMetadata(@NonNull final Builder builder) { - mVersionId = builder.mVersionId; - mCreatedAtMillis = builder.mCreatedAtMillis; - mAlgorithm = builder.mAlgorithm; - mKeySize = builder.mKeySize; - mIsDeprecated = builder.mIsDeprecated; - } - - /** - * Returns the key version identifier. - * - * @return non-null version id string. - */ - @NonNull - public String getVersionId() { - return mVersionId; - } - - /** - * Returns the Unix timestamp (milliseconds) at which this key was created. - * - * @return creation time in epoch milliseconds. - */ - public long getCreatedAtMillis() { - return mCreatedAtMillis; - } - - /** - * Returns the encryption algorithm string for this key. - * - * @return non-null algorithm string. - */ - @NonNull - public String getAlgorithm() { - return mAlgorithm; - } - - /** - * Returns the key size in bits. - * - * @return key size in bits. - */ - public int getKeySize() { - return mKeySize; - } + @Builder.Default + private final boolean deprecated = false; /** - * Returns whether this key is deprecated. A deprecated key may only be used for - * decryption; new data must be encrypted with a non-deprecated key. + * All-args constructor called by the Lombok-generated builder; validates required fields. * - * @return {@code true} if the key is deprecated. + * @throws IllegalStateException if {@code versionId} is null or blank. + * @throws IllegalArgumentException if {@code keySize} is not positive. */ - public boolean isDeprecated() { - return mIsDeprecated; + private KeyMetadata(final String versionId, final long createdAtMillis, + final String algorithm, final int keySize, final boolean deprecated) { + if (versionId == null || versionId.trim().isEmpty()) { + throw new IllegalStateException("versionId must be a non-blank string."); + } + if (keySize <= 0) { + throw new IllegalArgumentException("keySize must be a positive value."); + } + this.versionId = versionId; + this.createdAtMillis = createdAtMillis; + this.algorithm = algorithm; + this.keySize = keySize; + this.deprecated = deprecated; } /** @@ -139,120 +113,35 @@ public boolean isDeprecated() { @NonNull public String toJson() throws JSONException { final JSONObject json = new JSONObject(); - json.put(FIELD_VERSION_ID, mVersionId); - json.put(FIELD_CREATED_AT_MILLIS, mCreatedAtMillis); - json.put(FIELD_ALGORITHM, mAlgorithm); - json.put(FIELD_KEY_SIZE, mKeySize); - json.put(FIELD_IS_DEPRECATED, mIsDeprecated); + json.put(FIELD_VERSION_ID, versionId); + json.put(FIELD_CREATED_AT_MILLIS, createdAtMillis); + json.put(FIELD_ALGORITHM, algorithm); + json.put(FIELD_KEY_SIZE, keySize); + json.put(FIELD_IS_DEPRECATED, deprecated); return json.toString(); } /** * Deserializes a {@link KeyMetadata} instance from a JSON string. * + *

{@code algorithm}, {@code keySize}, and {@code deprecated} are optional in the JSON; + * missing fields fall back to their defaults ({@link #DEFAULT_ALGORITHM}, + * {@link #DEFAULT_KEY_SIZE}, and {@code false} respectively). Only {@code versionId} and + * {@code createdAtMillis} are required.

+ * * @param json the JSON string produced by {@link #toJson()}. * @return a reconstructed {@link KeyMetadata} instance. - * @throws JSONException if {@code json} is malformed or is missing required fields. + * @throws JSONException if {@code json} is malformed or missing required fields. */ @NonNull public static KeyMetadata fromJson(@NonNull final String json) throws JSONException { - final JSONObject jsonObject = new JSONObject(json); - return new Builder() - .versionId(jsonObject.getString(FIELD_VERSION_ID)) - .createdAtMillis(jsonObject.getLong(FIELD_CREATED_AT_MILLIS)) - .algorithm(jsonObject.getString(FIELD_ALGORITHM)) - .keySize(jsonObject.getInt(FIELD_KEY_SIZE)) - .isDeprecated(jsonObject.getBoolean(FIELD_IS_DEPRECATED)) + final JSONObject obj = new JSONObject(json); + return KeyMetadata.builder() + .versionId(obj.getString(FIELD_VERSION_ID)) + .createdAtMillis(obj.getLong(FIELD_CREATED_AT_MILLIS)) + .algorithm(obj.optString(FIELD_ALGORITHM, DEFAULT_ALGORITHM)) + .keySize(obj.optInt(FIELD_KEY_SIZE, DEFAULT_KEY_SIZE)) + .deprecated(obj.optBoolean(FIELD_IS_DEPRECATED, false)) .build(); } - - /** - * Builder for constructing {@link KeyMetadata} instances. - */ - public static final class Builder { - - private String mVersionId; - private long mCreatedAtMillis; - private String mAlgorithm = DEFAULT_ALGORITHM; - private int mKeySize = DEFAULT_KEY_SIZE; - private boolean mIsDeprecated = false; - - /** - * Sets the key version identifier. - * - * @param versionId non-null version id, e.g. {@code "K001"}. - * @return this builder. - */ - @NonNull - public Builder versionId(@NonNull final String versionId) { - mVersionId = versionId; - return this; - } - - /** - * Sets the creation timestamp. - * - * @param createdAtMillis Unix timestamp in milliseconds. - * @return this builder. - */ - @NonNull - public Builder createdAtMillis(final long createdAtMillis) { - mCreatedAtMillis = createdAtMillis; - return this; - } - - /** - * Sets the encryption algorithm. Defaults to {@link #DEFAULT_ALGORITHM}. - * - * @param algorithm non-null algorithm string. - * @return this builder. - */ - @NonNull - public Builder algorithm(@NonNull final String algorithm) { - mAlgorithm = algorithm; - return this; - } - - /** - * Sets the key size in bits. Defaults to {@link #DEFAULT_KEY_SIZE}. - * - * @param keySize key size in bits; must be positive. - * @return this builder. - * @throws IllegalArgumentException if {@code keySize} is not positive. - */ - @NonNull - public Builder keySize(final int keySize) { - if (keySize <= 0) { - throw new IllegalArgumentException("keySize must be a positive value."); - } - mKeySize = keySize; - return this; - } - - /** - * Sets whether the key is deprecated. - * - * @param isDeprecated {@code true} if this key should only be used for decryption. - * @return this builder. - */ - @NonNull - public Builder isDeprecated(final boolean isDeprecated) { - mIsDeprecated = isDeprecated; - return this; - } - - /** - * Builds a new {@link KeyMetadata} instance. - * - * @return a new {@link KeyMetadata}. - * @throws IllegalStateException if {@code versionId} has not been set. - */ - @NonNull - public KeyMetadata build() { - if (mVersionId == null || mVersionId.isEmpty()) { - throw new IllegalStateException("versionId must be set before calling build()."); - } - return new KeyMetadata(this); - } - } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java index fadb8060ef..336149e79c 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/crypto/KeyMetadataTest.java @@ -23,6 +23,7 @@ package com.microsoft.identity.common.java.crypto; import org.json.JSONException; +import org.json.JSONObject; import org.junit.Assert; import org.junit.Test; @@ -35,12 +36,12 @@ public class KeyMetadataTest { @Test public void testBuilder_setsAllFields() { - final KeyMetadata metadata = new KeyMetadata.Builder() + final KeyMetadata metadata = KeyMetadata.builder() .versionId(VERSION_ID) .createdAtMillis(CREATED_AT_MILLIS) .algorithm(ALGORITHM) .keySize(KEY_SIZE) - .isDeprecated(false) + .deprecated(false) .build(); Assert.assertEquals(VERSION_ID, metadata.getVersionId()); @@ -52,7 +53,7 @@ public void testBuilder_setsAllFields() { @Test public void testBuilder_defaultValues() { - final KeyMetadata metadata = new KeyMetadata.Builder() + final KeyMetadata metadata = KeyMetadata.builder() .versionId(VERSION_ID) .createdAtMillis(CREATED_AT_MILLIS) .build(); @@ -64,14 +65,14 @@ public void testBuilder_defaultValues() { @Test(expected = IllegalStateException.class) public void testBuilder_throwsWhenVersionIdMissing() { - new KeyMetadata.Builder() + KeyMetadata.builder() .createdAtMillis(CREATED_AT_MILLIS) .build(); } @Test(expected = IllegalStateException.class) public void testBuilder_throwsWhenVersionIdEmpty() { - new KeyMetadata.Builder() + KeyMetadata.builder() .versionId("") .createdAtMillis(CREATED_AT_MILLIS) .build(); @@ -79,35 +80,33 @@ public void testBuilder_throwsWhenVersionIdEmpty() { @Test public void testToJson_producesValidJson() throws JSONException { - final KeyMetadata metadata = new KeyMetadata.Builder() + final KeyMetadata metadata = KeyMetadata.builder() .versionId(VERSION_ID) .createdAtMillis(CREATED_AT_MILLIS) .algorithm(ALGORITHM) .keySize(KEY_SIZE) - .isDeprecated(true) + .deprecated(true) .build(); - final String json = metadata.toJson(); - Assert.assertNotNull(json); - Assert.assertTrue(json.contains(VERSION_ID)); - Assert.assertTrue(json.contains(String.valueOf(CREATED_AT_MILLIS))); - Assert.assertTrue(json.contains(ALGORITHM)); - Assert.assertTrue(json.contains(String.valueOf(KEY_SIZE))); - Assert.assertTrue(json.contains("true")); // isDeprecated + final JSONObject json = new JSONObject(metadata.toJson()); + Assert.assertEquals(VERSION_ID, json.getString("versionId")); + Assert.assertEquals(CREATED_AT_MILLIS, json.getLong("createdAtMillis")); + Assert.assertEquals(ALGORITHM, json.getString("algorithm")); + Assert.assertEquals(KEY_SIZE, json.getInt("keySize")); + Assert.assertTrue(json.getBoolean("isDeprecated")); } @Test public void testFromJson_reconstructsObject() throws JSONException { - final KeyMetadata original = new KeyMetadata.Builder() + final KeyMetadata original = KeyMetadata.builder() .versionId(VERSION_ID) .createdAtMillis(CREATED_AT_MILLIS) .algorithm(ALGORITHM) .keySize(KEY_SIZE) - .isDeprecated(false) + .deprecated(false) .build(); - final String json = original.toJson(); - final KeyMetadata reconstructed = KeyMetadata.fromJson(json); + final KeyMetadata reconstructed = KeyMetadata.fromJson(original.toJson()); Assert.assertEquals(original.getVersionId(), reconstructed.getVersionId()); Assert.assertEquals(original.getCreatedAtMillis(), reconstructed.getCreatedAtMillis()); @@ -118,10 +117,10 @@ public void testFromJson_reconstructsObject() throws JSONException { @Test public void testFromJson_reconstructsDeprecatedKey() throws JSONException { - final KeyMetadata original = new KeyMetadata.Builder() + final KeyMetadata original = KeyMetadata.builder() .versionId("K002") .createdAtMillis(CREATED_AT_MILLIS) - .isDeprecated(true) + .deprecated(true) .build(); final KeyMetadata reconstructed = KeyMetadata.fromJson(original.toJson()); @@ -130,6 +129,18 @@ public void testFromJson_reconstructsDeprecatedKey() throws JSONException { Assert.assertTrue(reconstructed.isDeprecated()); } + @Test + public void testFromJson_usesDefaultsForOptionalFields() throws JSONException { + // JSON with only required fields + final String minimalJson = "{\"versionId\":\"K003\",\"createdAtMillis\":1700000000000}"; + final KeyMetadata metadata = KeyMetadata.fromJson(minimalJson); + + Assert.assertEquals("K003", metadata.getVersionId()); + Assert.assertEquals(KeyMetadata.DEFAULT_ALGORITHM, metadata.getAlgorithm()); + Assert.assertEquals(KeyMetadata.DEFAULT_KEY_SIZE, metadata.getKeySize()); + Assert.assertFalse(metadata.isDeprecated()); + } + @Test(expected = JSONException.class) public void testFromJson_throwsOnMalformedJson() throws JSONException { KeyMetadata.fromJson("not valid json"); @@ -137,7 +148,7 @@ public void testFromJson_throwsOnMalformedJson() throws JSONException { @Test(expected = IllegalArgumentException.class) public void testBuilder_throwsOnInvalidKeySize() { - new KeyMetadata.Builder() + KeyMetadata.builder() .versionId(VERSION_ID) .createdAtMillis(CREATED_AT_MILLIS) .keySize(0)