diff --git a/conf/default-config.json b/conf/default-config.json index 3b9507e7..5851e70c 100644 --- a/conf/default-config.json +++ b/conf/default-config.json @@ -17,5 +17,6 @@ "att_token_enc_key": null, "att_token_enc_salt": null, "enforceJwt": false, - "cloud_encryption_keys_metadata_path": null + "cloud_encryption_keys_metadata_path": null, + "encryption_support_version": "9999" } diff --git a/conf/integ-config.json b/conf/integ-config.json index fa8b1049..8432bc22 100644 --- a/conf/integ-config.json +++ b/conf/integ-config.json @@ -18,5 +18,6 @@ "keyset_keys_metadata_path": "uid2/keyset_keys/metadata.json", "salts_metadata_path": "uid2/salts/metadata.json", "enforceJwt": false, - "cloud_encryption_keys_metadata_path": "uid2/cloud_encryption_keys/metadata.json" + "cloud_encryption_keys_metadata_path": "uid2/cloud_encryption_keys/metadata.json", + "encryption_support_version": "9999" } \ No newline at end of file diff --git a/conf/local-config.json b/conf/local-config.json index d983fca6..6e119b8a 100644 --- a/conf/local-config.json +++ b/conf/local-config.json @@ -19,5 +19,6 @@ "att_token_enc_salt": "", "provide_private_site_data": true, "enforceJwt": false, - "cloud_encryption_keys_metadata_path": "/com.uid2.core/test/cloud_encryption_keys/metadata.json" + "cloud_encryption_keys_metadata_path": "/com.uid2.core/test/cloud_encryption_keys/metadata.json", + "encryption_support_version": "9999" } diff --git a/conf/local-e2e-config.json b/conf/local-e2e-config.json index b4ed344d..fec4459f 100644 --- a/conf/local-e2e-config.json +++ b/conf/local-e2e-config.json @@ -33,5 +33,7 @@ "aws_kms_jwt_signing_public_keys": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmvwB41qI5Fe41PDbXqcX5uOvSvfKh8l9QV0O3M+NsB4lKqQEP0t1hfoiXTpOgKz1ArYxHsQ2LeXifX4uwEbYJFlpVM+tyQkTWQjBOw6fsLYK2Xk4X2ylNXUUf7x3SDiOVxyvTh3OZW9kqrDBN9JxSoraNLyfw0hhW0SHpfs699SehgbQ7QWep/gVlKRLIz0XAXaZNw24s79ORcQlrCE6YD0PgQmpI/dK5xMML82n6y3qcTlywlGaU7OGIMdD+CTXA3BcOkgXeqZTXNaX1u6jCTa1lvAczun6avp5VZ4TFiuPo+y4rJ3GU+14cyT5NckEcaTKSvd86UdwK5Id9tl3bQIDAQAB", "core_public_url": "http://localhost:8088", "optout_url": "http://localhost:8081", - "cloud_keys_metadata_path": "cloud_encryption_keys/metadata.json" + "s3_keys_metadata_path": "s3encryption_keys/metadata.json", + "cloud_keys_metadata_path": "cloud_encryption_keys/metadata.json", + "encryption_support_version": "6.0.0" } diff --git a/conf/local-e2e-docker-config.json b/conf/local-e2e-docker-config.json index fb0532af..cc9bb3ac 100644 --- a/conf/local-e2e-docker-config.json +++ b/conf/local-e2e-docker-config.json @@ -32,5 +32,6 @@ "aws_kms_jwt_signing_public_keys": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmvwB41qI5Fe41PDbXqcX5uOvSvfKh8l9QV0O3M+NsB4lKqQEP0t1hfoiXTpOgKz1ArYxHsQ2LeXifX4uwEbYJFlpVM+tyQkTWQjBOw6fsLYK2Xk4X2ylNXUUf7x3SDiOVxyvTh3OZW9kqrDBN9JxSoraNLyfw0hhW0SHpfs699SehgbQ7QWep/gVlKRLIz0XAXaZNw24s79ORcQlrCE6YD0PgQmpI/dK5xMML82n6y3qcTlywlGaU7OGIMdD+CTXA3BcOkgXeqZTXNaX1u6jCTa1lvAczun6avp5VZ4TFiuPo+y4rJ3GU+14cyT5NckEcaTKSvd86UdwK5Id9tl3bQIDAQAB", "core_public_url": "http://core:8088", "optout_url": "http://optout:8081", - "cloud_encryption_keys_metadata_path": "cloud_encryption_keys/metadata.json" + "cloud_encryption_keys_metadata_path": "cloud_encryption_keys/metadata.json", + "encryption_support_version": "6.0.0" } diff --git a/pom.xml b/pom.xml index 5e47ad00..505603a0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-core - 2.21.7 + 2.21.8-alpha-63-SNAPSHOT UTF-8 @@ -172,135 +172,139 @@ 5.12.0 test + + org.mockito + mockito-inline + 5.2.0 + test + + + net.bytebuddy + byte-buddy + 1.14.17 + + - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - - - prepare-agent - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.12.1 - - 21 - 21 - 21 - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - io.reactiverse - vertx-maven-plugin - ${vertx-maven-plugin.version} - - - vmp - - initialize - package - - - - - true - - -Djava.security.egd=file:/dev/./urandom - - - - - org.apache.maven.plugins - maven-assembly-plugin - 3.3.0 - - - - jar-with-dependencies - - - - - com.uid2.core.Main - - - - - - make-assembly - - package - - single - - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0.0 - - - generate-resources - - write-project-properties - - - ${project.build.outputDirectory}/${project.artifactId}.properties - - - - - - org.codehaus.mojo - exec-maven-plugin - ${exec-maven-plugin.version} - - - default-cli - - java - - - com.uid2.core.Main - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - com.azure.tools - azure-sdk-build-tool - 1.0.0 - - - + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + + prepare-agent + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 16 + 16 + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + io.reactiverse + vertx-maven-plugin + ${vertx-maven-plugin.version} + + + vmp + + initialize + package + + + + + true + + -Djava.security.egd=file:/dev/./urandom + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + jar-with-dependencies + + + + + com.uid2.core.Main + + + + + + make-assembly + + package + + single + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + generate-resources + + write-project-properties + + + ${project.build.outputDirectory}/${project.artifactId}.properties + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec-maven-plugin.version} + + + default-cli + + java + + + com.uid2.core.Main + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + diff --git a/src/main/java/com/uid2/core/service/ClientMetadataProvider.java b/src/main/java/com/uid2/core/service/ClientMetadataProvider.java index 1ad85f50..36935274 100644 --- a/src/main/java/com/uid2/core/service/ClientMetadataProvider.java +++ b/src/main/java/com/uid2/core/service/ClientMetadataProvider.java @@ -23,7 +23,7 @@ public class ClientMetadataProvider implements IClientMetadataProvider { @Override public String getMetadata(OperatorInfo info) throws Exception { - String pathname = getMetadataPathName(info.getOperatorType(), info.getSiteId(), SecretStore.Global.get(ClientsMetadataPathName)); + String pathname = getMetadataPathName(info, SecretStore.Global.get(ClientsMetadataPathName)); String original = readToEndAsString(metadataStreamProvider.download(pathname)); JsonObject main = (JsonObject) Json.decodeValue(original); JsonObject obj = main.getJsonObject("client_keys"); diff --git a/src/main/java/com/uid2/core/service/ISiteMetadataProvider.java b/src/main/java/com/uid2/core/service/ISiteMetadataProvider.java index 6e182296..993f55fd 100644 --- a/src/main/java/com/uid2/core/service/ISiteMetadataProvider.java +++ b/src/main/java/com/uid2/core/service/ISiteMetadataProvider.java @@ -1,5 +1,7 @@ package com.uid2.core.service; +import com.uid2.core.util.OperatorInfo; + public interface ISiteMetadataProvider { - String getMetadata() throws Exception; + String getMetadata(OperatorInfo info) throws Exception; } diff --git a/src/main/java/com/uid2/core/service/KeysetKeysMetadataProvider.java b/src/main/java/com/uid2/core/service/KeysetKeysMetadataProvider.java index 93534fab..f013ee2d 100644 --- a/src/main/java/com/uid2/core/service/KeysetKeysMetadataProvider.java +++ b/src/main/java/com/uid2/core/service/KeysetKeysMetadataProvider.java @@ -21,7 +21,7 @@ public KeysetKeysMetadataProvider(ICloudStorage cloudStorage) { @Override public String getMetadata(OperatorInfo info) throws Exception { - String pathname = getMetadataPathName(info.getOperatorType(), info.getSiteId(), SecretStore.Global.get(Const.Config.KeysetKeysMetadataPathProp)); + String pathname = getMetadataPathName(info, SecretStore.Global.get(Const.Config.KeysetKeysMetadataPathProp)); String original = readToEndAsString(metadataStreamProvider.download(pathname)); JsonObject main = (JsonObject) Json.decodeValue(original); JsonObject obj = main.getJsonObject("keyset_keys"); diff --git a/src/main/java/com/uid2/core/service/KeysetMetadataProvider.java b/src/main/java/com/uid2/core/service/KeysetMetadataProvider.java index 4068730d..208eea85 100644 --- a/src/main/java/com/uid2/core/service/KeysetMetadataProvider.java +++ b/src/main/java/com/uid2/core/service/KeysetMetadataProvider.java @@ -20,7 +20,7 @@ public KeysetMetadataProvider(ICloudStorage cloudStorage) { @Override public String getMetadata(OperatorInfo info) throws Exception { - String pathname = getMetadataPathName(info.getOperatorType(), info.getSiteId(), SecretStore.Global.get(Const.Config.KeysetsMetadataPathProp)); + String pathname = getMetadataPathName(info, SecretStore.Global.get(Const.Config.KeysetsMetadataPathProp)); String original = readToEndAsString(metadataStreamProvider.download(pathname)); JsonObject main = (JsonObject) Json.decodeValue(original); JsonObject obj = main.getJsonObject("keysets"); diff --git a/src/main/java/com/uid2/core/service/SiteMetadataProvider.java b/src/main/java/com/uid2/core/service/SiteMetadataProvider.java index 4d4bae14..4f95ab83 100644 --- a/src/main/java/com/uid2/core/service/SiteMetadataProvider.java +++ b/src/main/java/com/uid2/core/service/SiteMetadataProvider.java @@ -1,6 +1,8 @@ package com.uid2.core.service; import com.uid2.core.model.SecretStore; +import com.uid2.core.util.OperatorInfo; +import com.uid2.shared.Const; import com.uid2.shared.cloud.ICloudStorage; import com.uid2.shared.store.CloudPath; import com.uid2.shared.store.scope.GlobalScope; @@ -11,6 +13,7 @@ import java.io.InputStream; import java.io.InputStreamReader; +import static com.uid2.core.util.MetadataHelper.getMetadataPathName; import static com.uid2.core.util.MetadataHelper.readToEndAsString; public class SiteMetadataProvider implements ISiteMetadataProvider { @@ -22,8 +25,8 @@ public SiteMetadataProvider(ICloudStorage cloudStorage) { this.metadataStreamProvider = this.downloadUrlGenerator = cloudStorage; } @Override - public String getMetadata() throws Exception { - String pathname = new GlobalScope(new CloudPath(SecretStore.Global.get(SiteMetadataPathName))).getMetadataPath().toString(); + public String getMetadata(OperatorInfo info) throws Exception { + String pathname = getMetadataPathName(info, SecretStore.Global.get(SiteMetadataPathName)); String original = readToEndAsString(metadataStreamProvider.download(pathname)); JsonObject main = (JsonObject) Json.decodeValue(original); JsonObject obj = main.getJsonObject("sites"); diff --git a/src/main/java/com/uid2/core/util/MetadataHelper.java b/src/main/java/com/uid2/core/util/MetadataHelper.java index 1e519af6..4110aca6 100644 --- a/src/main/java/com/uid2/core/util/MetadataHelper.java +++ b/src/main/java/com/uid2/core/util/MetadataHelper.java @@ -4,6 +4,7 @@ import com.uid2.shared.auth.OperatorType; import com.uid2.shared.auth.Role; import com.uid2.shared.store.CloudPath; +import com.uid2.shared.store.scope.EncryptedScope; import com.uid2.shared.store.scope.GlobalScope; import com.uid2.shared.store.scope.SiteScope; import com.uid2.shared.store.scope.StoreScope; @@ -22,17 +23,34 @@ public static String getSiteSpecificMetadataPathName(int siteId, String metadata return SiteSpecificDataSubDirPath +siteId + metadataPathName; } - public static String getMetadataPathName(OperatorType operatorType, int siteId, String metadataPathName) - { + public static String getMetadataPathName(OperatorType operatorType, int siteId, String metadataPathName) { + return getMetadataPathName(operatorType, siteId, metadataPathName, false); + } + + public static String getMetadataPathName(OperatorInfo info, String metadataPathName) { + return getMetadataPathName(info.getOperatorType(), info.getSiteId(), metadataPathName, info.getSupportsEncryption()); + } + + + public static String getMetadataPathName(OperatorType operatorType, int siteId, String metadataPathName, Boolean supportsDecryption) { StoreScope store; Boolean providePrivateSiteData = ConfigStore.Global.getBoolean("provide_private_site_data"); - if (operatorType == OperatorType.PUBLIC || (providePrivateSiteData == null || !providePrivateSiteData.booleanValue())) - { - store = new GlobalScope(new CloudPath(metadataPathName)); - } - else //PRIVATE - { - store = new SiteScope(new CloudPath(metadataPathName), siteId); + if (supportsDecryption) { // Check if decryption is possible + if (operatorType == OperatorType.PUBLIC ) //siteId_public folder + { + store = new EncryptedScope(new CloudPath(metadataPathName), siteId, true); + } else //siteId_private folder + { + store = new EncryptedScope(new CloudPath(metadataPathName), siteId, false); + } + } else { + if (operatorType == OperatorType.PUBLIC || (providePrivateSiteData == null || !providePrivateSiteData.booleanValue())) + { + store = new GlobalScope(new CloudPath(metadataPathName)); + } else //PRIVATE + { + store = new SiteScope(new CloudPath(metadataPathName), siteId); + } } return store.getMetadataPath().toString(); } diff --git a/src/main/java/com/uid2/core/util/OperatorInfo.java b/src/main/java/com/uid2/core/util/OperatorInfo.java index 2ec97020..091853c2 100644 --- a/src/main/java/com/uid2/core/util/OperatorInfo.java +++ b/src/main/java/com/uid2/core/util/OperatorInfo.java @@ -1,9 +1,19 @@ package com.uid2.core.util; +import com.uid2.core.Const; import com.uid2.shared.auth.IAuthorizable; import com.uid2.shared.auth.OperatorKey; import com.uid2.shared.auth.OperatorType; +import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.uid2.core.model.ConfigStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.uid2.shared.Const.Config.encryptionSupportVersion; +import static com.uid2.shared.Const.Http.AppVersionHeader; import static com.uid2.shared.middleware.AuthMiddleware.API_CLIENT_PROP; /** @@ -13,6 +23,9 @@ public class OperatorInfo { private final OperatorType operatorType; private final int siteId; + private final boolean supportsEncryption; + + static Logger logger = LoggerFactory.getLogger(OperatorInfo.class); public OperatorType getOperatorType() { return operatorType; @@ -22,17 +35,72 @@ public int getSiteId() { return siteId; } - public OperatorInfo(OperatorType operatorType, int siteId) { + public boolean getSupportsEncryption() {return supportsEncryption;} + + public OperatorInfo(OperatorType operatorType, int siteId, boolean supportsEncryption) { this.operatorType = operatorType; this.siteId = siteId; + this.supportsEncryption = supportsEncryption; } public static OperatorInfo getOperatorInfo(RoutingContext rc) throws Exception { IAuthorizable profile = (IAuthorizable) rc.data().get(API_CLIENT_PROP); if (profile instanceof OperatorKey) { OperatorKey operatorKey = (OperatorKey) profile; - return new OperatorInfo(operatorKey.getOperatorType(), operatorKey.getSiteId()); + return new OperatorInfo(operatorKey.getOperatorType(), operatorKey.getSiteId(), supportsEncryption(rc)); } throw new Exception("Cannot determine the operator type and site id from the profile"); } + + static boolean supportsEncryption(RoutingContext rc) { + String appVersion = rc.request().getHeader(AppVersionHeader); + if (appVersion == null) { + logger.warn("AppVersion header is missing."); + return false; + } + String[] versions = appVersion.split(";"); + for (String version : versions) { + if (version.startsWith("uid2-operator=")) { + String operatorVersion = version.substring("uid2-operator=".length()); + boolean isSupported = isVersionGreaterOrEqual(operatorVersion, ConfigStore.Global.getOrDefault(encryptionSupportVersion, "9999")); + logger.debug("Operator version: {}, {}", + operatorVersion, isSupported ? "Supports encryption" : "Does not support encryption"); + return isSupported; + } + } + return false; + } + + /* + Returns if the version of a semvar v1 is greater or equal to v2 + */ + static boolean isVersionGreaterOrEqual(String v1, String v2) { + Pattern pattern = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?"); + Matcher m1 = pattern.matcher(v1); + Matcher m2 = pattern.matcher(v2); + + int[] parts1 = extractParts(m1); + int[] parts2 = extractParts(m2); + + for (int i = 0; i < Math.max(parts1.length, parts2.length); i++) { + int p1 = i < parts1.length ? parts1[i] : 0; + int p2 = i < parts2.length ? parts2[i] : 0; + if (p1 != p2) { + return p1 > p2; + } + } + + return true; + } + + private static int[] extractParts(Matcher matcher) { + int[] parts = new int[3]; + if (matcher.find()) { + for (int i = 1; i <= 3; i++) { + String group = matcher.group(i); + parts[i - 1] = group != null ? Integer.parseInt(group) : 0; + } + } + return parts; + } } \ No newline at end of file diff --git a/src/main/java/com/uid2/core/vertx/CoreVerticle.java b/src/main/java/com/uid2/core/vertx/CoreVerticle.java index b76d3156..50485580 100644 --- a/src/main/java/com/uid2/core/vertx/CoreVerticle.java +++ b/src/main/java/com/uid2/core/vertx/CoreVerticle.java @@ -53,7 +53,6 @@ import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider; import com.uid2.shared.model.CloudEncryptionKey; - import static com.uid2.shared.Const.Config.EnforceJwtProp; public class CoreVerticle extends AbstractVerticle { @@ -261,17 +260,17 @@ private void handleAttestAsync(RoutingContext rc) { if (!attestationResult.isSuccess()) { AttestationFailure failure = attestationResult.getFailure(); switch (failure) { - case AttestationFailure.BAD_FORMAT: - case AttestationFailure.INVALID_PROTOCOL: - case AttestationFailure.BAD_CERTIFICATE: - case AttestationFailure.BAD_PAYLOAD: - case AttestationFailure.UNKNOWN_ATTESTATION_URL: - case AttestationFailure.FORBIDDEN_ENCLAVE: + case BAD_FORMAT: + case INVALID_PROTOCOL: + case BAD_CERTIFICATE: + case BAD_PAYLOAD: + case UNKNOWN_ATTESTATION_URL: + case FORBIDDEN_ENCLAVE: setAttestationFailureReason(rc, failure, Collections.singletonMap("reason", attestationResult.getReason())); Error(attestationResult.getReason(), 403, rc, failure.explain()); return; - case AttestationFailure.UNKNOWN: - case AttestationFailure.INTERNAL_ERROR: + case UNKNOWN: + case INTERNAL_ERROR: setAttestationFailureReason(rc, failure, Collections.singletonMap("reason", attestationResult.getReason())); Error(attestationResult.getReason(), 500, rc, failure.explain()); return; @@ -384,7 +383,7 @@ private void handleSiteRefresh(RoutingContext rc) { return; } rc.response().putHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .end(siteMetadataProvider.getMetadata()); + .end(siteMetadataProvider.getMetadata(info)); } catch (Exception e) { logger.warn("exception in handleSiteRefresh: " + e.getMessage(), e); Error("error", 500, rc, "error processing sites refresh"); diff --git a/src/main/resources/com.uid2.core/test/operators/operators.json b/src/main/resources/com.uid2.core/test/operators/operators.json index 8da78f31..795783cc 100644 --- a/src/main/resources/com.uid2.core/test/operators/operators.json +++ b/src/main/resources/com.uid2.core/test/operators/operators.json @@ -1,4 +1,21 @@ [ + { + "key": "UID2-O-L-999-dp9Dt0.JVoGpynN4J8nMA7FxmzsavxJa8B9H74y9xdEE=", + "name": "Special", + "contact": "Special", + "protocol": "trusted", + "created": 1701210253, + "disabled": false, + "roles": [ + "OPERATOR", + "OPTOUT" + ], + "site_id": 999, + "operator_type": "PUBLIC", + "key_hash": "rTD7MpJn5/j4G6N+Ph659F4FGtiJy7MLNtfVA7XUdu6cYC9ok6EwGeI2upyDOvxvPkOCUn7HBKay8ubPQmRc0A==", + "key_salt": "ZpqdDFksFeWx/ouPAoWi39TVuGrSGwijfCN4f0pAl2Y=", + "key_id": "UID2-O-L-999-dp9Dt" + }, { "key": "test-partner-key", "name": "partner@uid2.com", diff --git a/src/test/java/com/uid2/core/util/TestMetadataHelper.java b/src/test/java/com/uid2/core/util/TestMetadataHelper.java new file mode 100644 index 00000000..5d5f8071 --- /dev/null +++ b/src/test/java/com/uid2/core/util/TestMetadataHelper.java @@ -0,0 +1,67 @@ +package com.uid2.core.util; + +import com.uid2.core.model.ConfigStore; +import com.uid2.shared.auth.OperatorType; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + + +public class TestMetadataHelper { + + @Mock + private OperatorInfo operatorInfo; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ConfigStore.Global.load(new JsonObject().put("provide_private_site_data", true)); + } + + @Test + void testGetMetadataPathNameDecryptPublic() { + when(operatorInfo.getOperatorType()).thenReturn(OperatorType.PUBLIC); + when(operatorInfo.getSiteId()).thenReturn(42); + when(operatorInfo.getSupportsEncryption()).thenReturn(true); + + String result = MetadataHelper.getMetadataPathName(operatorInfo, "s3://test-bucket/folder/"); + assertEquals("s3://test-bucket/encrypted/42_public/folder", result); + } + + @Test + void testGetMetadataPathNameDecryptPrivate() { + when(operatorInfo.getOperatorType()).thenReturn(OperatorType.PRIVATE); + when(operatorInfo.getSiteId()).thenReturn(42); + when(operatorInfo.getSupportsEncryption()).thenReturn(true); + + String result = MetadataHelper.getMetadataPathName(operatorInfo, "s3://test-bucket/folder/"); + assertEquals("s3://test-bucket/encrypted/42_private/folder", result); + } + + + @Test + void testGetMetadataPathNamePublic() { + when(operatorInfo.getOperatorType()).thenReturn(OperatorType.PUBLIC); + when(operatorInfo.getSiteId()).thenReturn(42); + when(operatorInfo.getSupportsEncryption()).thenReturn(false); + + String result = MetadataHelper.getMetadataPathName(operatorInfo, "s3://test-bucket/folder/"); + assertEquals("s3://test-bucket/folder", result); + } + + + @Test + void testGetMetadataPathNamePrivate() { + when(operatorInfo.getOperatorType()).thenReturn(OperatorType.PRIVATE); + when(operatorInfo.getSiteId()).thenReturn(42); + when(operatorInfo.getSupportsEncryption()).thenReturn(false); + + String result = MetadataHelper.getMetadataPathName(operatorInfo, "s3://test-bucket/folder/"); + assertEquals("s3://test-bucket/site/42/folder", result); + } +} diff --git a/src/test/java/com/uid2/core/util/TestOperatorInfo.java b/src/test/java/com/uid2/core/util/TestOperatorInfo.java new file mode 100644 index 00000000..9d3b8393 --- /dev/null +++ b/src/test/java/com/uid2/core/util/TestOperatorInfo.java @@ -0,0 +1,105 @@ +package com.uid2.core.util; + +import com.uid2.core.model.ConfigStore; +import com.uid2.shared.auth.OperatorKey; +import com.uid2.shared.auth.OperatorType; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.Map; + +import static com.uid2.shared.Const.Http.AppVersionHeader; +import static com.uid2.shared.middleware.AuthMiddleware.API_CLIENT_PROP; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class OperatorInfoTest { + + @Mock + private RoutingContext mockRoutingContext; + + @Mock + private HttpServerRequest mockRequest; + private static final String encryptionSupportVersion = "encryption_support_version"; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ConfigStore.Global.load(new JsonObject().put(encryptionSupportVersion, "2.6")); + when(mockRoutingContext.request()).thenReturn(mockRequest); + } + + @Test + void testConstructor() { + OperatorInfo operatorInfo = new OperatorInfo(OperatorType.PRIVATE, 123, true); + assertEquals(OperatorType.PRIVATE, operatorInfo.getOperatorType()); + assertEquals(123, operatorInfo.getSiteId()); + assertTrue(operatorInfo.getSupportsEncryption()); + } + + @Test + void testGetOperatorInfo() throws Exception { + OperatorKey mockOperatorKey = mock(OperatorKey.class); + when(mockOperatorKey.getOperatorType()).thenReturn(OperatorType.PUBLIC); + when(mockOperatorKey.getSiteId()).thenReturn(456); + + Map data = new HashMap<>(); + data.put(API_CLIENT_PROP, mockOperatorKey); + when(mockRoutingContext.data()).thenReturn(data); + when(mockRequest.getHeader(AppVersionHeader)).thenReturn("uid2-operator=3.0.0"); + + OperatorInfo result = OperatorInfo.getOperatorInfo(mockRoutingContext); + + assertNotNull(result); + assertEquals(OperatorType.PUBLIC, result.getOperatorType()); + assertEquals(456, result.getSiteId()); + assertTrue(result.getSupportsEncryption()); + } + + @Test + void testGetOperatorInfoThrowsException() { + Map data = new HashMap<>(); + data.put("api_client", "Invalid Object"); + when(mockRoutingContext.data()).thenReturn(data); + + assertThrows(Exception.class, () -> OperatorInfo.getOperatorInfo(mockRoutingContext)); + } + + @Test + void testSupportsEncryptionTrue() { + when(mockRequest.getHeader(AppVersionHeader)).thenReturn("uid2-operator=3.0.0"); + assertTrue(OperatorInfo.supportsEncryption(mockRoutingContext)); + } + + @Test + void testSupportsEncryptionFalse() { + when(mockRequest.getHeader(AppVersionHeader)).thenReturn("uid2-operator=1.0.0"); + assertFalse(OperatorInfo.supportsEncryption(mockRoutingContext)); + } + + @Test + void testSupportsEncryptionMissingHeader() { + when(mockRequest.getHeader(AppVersionHeader)).thenReturn(null); + assertFalse(OperatorInfo.supportsEncryption(mockRoutingContext)); + } + + @Test + void testIsVersionGreaterOrEqual() { + assertTrue(OperatorInfo.isVersionGreaterOrEqual("2.0.0", "1.0.0")); + assertTrue(OperatorInfo.isVersionGreaterOrEqual("2.0.0", "2.0.0")); + assertFalse(OperatorInfo.isVersionGreaterOrEqual("1.0.0", "2.0.0")); + assertTrue(OperatorInfo.isVersionGreaterOrEqual("2.1.0", "2.0.0")); + assertFalse(OperatorInfo.isVersionGreaterOrEqual("2.0.1", "2.1.0")); + assertFalse(OperatorInfo.isVersionGreaterOrEqual("operator.5.26.19-56899dc0d7", "operator.5.27.19-56899dc0d7")); + assertTrue(OperatorInfo.isVersionGreaterOrEqual("operator.5.27.19-56899dc0d7", "operator.5.27.19-56899dc0d7")); + assertTrue(OperatorInfo.isVersionGreaterOrEqual("operator.5.27.19-56899dc0d7", "operator.5.26.19-56899dc0d7")); + + assertTrue(OperatorInfo.isVersionGreaterOrEqual("uid2-operator.5.40.25-alpha-15-SNAPSHOT", "uid2-operator.5.40.25-alpha-15-SNAPSHOT")); + } +} \ No newline at end of file diff --git a/src/test/java/com/uid2/core/vertx/TestCoreVerticle.java b/src/test/java/com/uid2/core/vertx/TestCoreVerticle.java index f2191f09..dc271e95 100644 --- a/src/test/java/com/uid2/core/vertx/TestCoreVerticle.java +++ b/src/test/java/com/uid2/core/vertx/TestCoreVerticle.java @@ -1,7 +1,7 @@ package com.uid2.core.vertx; - import com.uid2.core.model.ConfigStore; -import com.uid2.core.service.AttestationService; +import com.uid2.core.model.SecretStore; +import com.uid2.core.service.*; import com.uid2.core.service.JWTTokenProvider; import com.uid2.core.service.OperatorJWTTokenProvider; import com.uid2.shared.Const; @@ -38,6 +38,8 @@ import org.mockito.MockitoAnnotations; import javax.crypto.Cipher; +import java.io.ByteArrayInputStream; +import java.net.URL; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.SecureRandom; @@ -80,18 +82,43 @@ void deployVerticle(TestInfo info, Vertx vertx, VertxTestContext testContext) th config.put(Const.Config.OptOutUrlProp, "test_optout_url"); config.put(Const.Config.CorePublicUrlProp, "test_core_url"); config.put(Const.Config.AwsKmsJwtSigningKeyIdProp, "test_aws_kms_keyId"); + config.put(Const.Config.KeysetsMetadataPathProp, "keysets/metadata.json"); + config.put(Const.Config.encryptionSupportVersion, "2.6"); if (info.getTags().contains("dontForceJwt")) { config.put(Const.Config.EnforceJwtProp, false); } else { config.put(Const.Config.EnforceJwtProp, true); } ConfigStore.Global.load(config); + SecretStore.Global.load(config); attestationService = new AttestationService(); MockitoAnnotations.initMocks(this); + // Mock download method for different paths + when(cloudStorage.download(anyString())).thenAnswer(invocation -> { + String path = invocation.getArgument(0); + System.out.println(path); + if (path.contains("encrypted")) { + return new ByteArrayInputStream("{ \"keysets\": { \"location\": \"encrypted-location\" } }".getBytes()); + } else { + return new ByteArrayInputStream("{ \"keysets\": { \"location\": \"default-location\" } }".getBytes()); + } + }); + + // Mock preSignUrl method for different paths + when(cloudStorage.preSignUrl(anyString())).thenAnswer(invocation -> { + String path = invocation.getArgument(0); + if (path.contains("encrypted")) { + return new URL("http://encrypted_url"); + }else { + return new URL("http://default_url"); + } + }); + CoreVerticle verticle = new CoreVerticle(cloudStorage, authProvider, attestationService, attestationTokenService, enclaveIdentifierProvider, operatorJWTTokenProvider, jwtService, cloudEncryptionKeyProvider); vertx.deployVerticle(verticle, testContext.succeeding(id -> testContext.completeNow())); + } private String getUrlForEndpoint(String endpoint) { @@ -142,6 +169,13 @@ private void get(Vertx vertx, String endpoint, Handler>> handler) { + WebClient client = WebClient.create(vertx); + client.getAbs(getUrlForEndpoint(endpoint)) + .putHeaders(headers) + .send(handler); + } + private void addAttestationProvider(String protocol) { attestationService.with(protocol, attestationProvider); } @@ -743,7 +777,6 @@ void cloudEncryptionKeyRetrieveNoKeysOrError(Vertx vertx, VertxTestContext testC assertEquals(500, response2.statusCode()); JsonObject json2 = response2.bodyAsJsonObject(); - System.out.println(json2); assertEquals("error", json2.getString("status")); assertEquals("error generating attestation token", json2.getString("message")); @@ -760,4 +793,85 @@ void cloudEncryptionKeyRetrieveNoKeysOrError(Vertx vertx, VertxTestContext testC } }); } + + @Tag("dontForceJwt") + @Test + void keysetRefreshSuccessHigherVersion(Vertx vertx, VertxTestContext testContext) throws Exception { + fakeAuth(attestationProtocolPublic, Role.OPERATOR); + addAttestationProvider(attestationProtocolPublic); + onHandleAttestationRequest(() -> { + byte[] resultPublicKey = null; + return Future.succeededFuture(new AttestationResult(resultPublicKey, "test")); + }); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(Const.Http.AppVersionHeader, "uid2-operator=3.7.16-SNAPSHOT;uid2-attestation-api=1.1.0;uid2-shared=2.7.0-3e279acefa"); + + getWithVersion(vertx, "key/keyset/refresh", headers, ar -> { + assertTrue(ar.succeeded()); + if (ar.succeeded()) { + HttpResponse response = ar.result(); + assertEquals(200, response.statusCode()); + String responseBody = response.bodyAsString(); + System.out.println(responseBody); + assertEquals("{\"keysets\":{\"location\":\"http://encrypted_url\"}}", responseBody); + testContext.completeNow(); + } else { + testContext.failNow(ar.cause()); + } + }); + } + + @Tag("dontForceJwt") + @Test + void keysRefreshSuccessLowerVersion(Vertx vertx, VertxTestContext testContext) throws Exception { + fakeAuth(attestationProtocolPublic, Role.OPERATOR); + addAttestationProvider(attestationProtocolPublic); + onHandleAttestationRequest(() -> { + byte[] resultPublicKey = null; + return Future.succeededFuture(new AttestationResult(resultPublicKey, "test")); + }); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + headers.add(Const.Http.AppVersionHeader, "uid2-operator=2.1.16-SNAPSHOT;uid2-attestation-api=1.1.0;uid2-shared=2.7.0-3e279acefa"); + + getWithVersion(vertx, "key/keyset/refresh", headers, ar -> { + if (ar.succeeded()) { + HttpResponse response = ar.result(); + System.out.println(response.bodyAsString()); + assertEquals(200, response.statusCode()); + String responseBody = response.bodyAsString(); + assertEquals("{\"keysets\":{\"location\":\"http://default_url\"}}", responseBody); + testContext.completeNow(); + } else { + testContext.failNow(ar.cause()); + } + }); + } + + @Tag("dontForceJwt") + @Test + void keysRefreshSuccessNoHeaderVersion(Vertx vertx, VertxTestContext testContext) throws Exception { + fakeAuth(attestationProtocolPublic, Role.OPERATOR); + addAttestationProvider(attestationProtocolPublic); + onHandleAttestationRequest(() -> { + byte[] resultPublicKey = null; + return Future.succeededFuture(new AttestationResult(resultPublicKey, "test")); + }); + + MultiMap headers = MultiMap.caseInsensitiveMultiMap(); + + getWithVersion(vertx, "key/keyset/refresh", headers, ar -> { + if (ar.succeeded()) { + HttpResponse response = ar.result(); + System.out.println(response.bodyAsString()); + assertEquals(200, response.statusCode()); + String responseBody = response.bodyAsString(); + assertEquals("{\"keysets\":{\"location\":\"http://default_url\"}}", responseBody); + testContext.completeNow(); + } else { + testContext.failNow(ar.cause()); + } + }); + } } diff --git a/src/test/resources/com.uid2.core/model/test-config.json b/src/test/resources/com.uid2.core/model/test-config.json index efafe6b4..40d3e07a 100644 --- a/src/test/resources/com.uid2.core/model/test-config.json +++ b/src/test/resources/com.uid2.core/model/test-config.json @@ -23,5 +23,5 @@ "att_token_enc_salt": "", "att_token_lifetime_seconds": 120, "provide_private_site_data": true, - "cloud_keys_metadata_path": "cloud_encryption_keys/metadata.json" + "cloud_encryption_keys_metadata_path": "cloud_encryption_keys/metadata.json" }