diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/FakeKeyService.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/FakeKeyService.java index cf79e266..ee130014 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/FakeKeyService.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/FakeKeyService.java @@ -63,18 +63,19 @@ private void deleteAllKeys() { this.dataService.cleanDB(Duration.ofDays(0)); } - public List fillUpKeys(List keys, Long publishedafter, Long keyDate) { + public List fillUpKeys( + List keys, UTCInstant publishedafter, UTCInstant keyDate, UTCInstant now) { if (!isEnabled) { return keys; } var today = UTCInstant.today(); - var keyLocalDate = UTCInstant.ofEpochMillis(keyDate).atStartOfDay(); + var keyLocalDate = keyDate.atStartOfDay(); if (today.hasSameDateAs(keyLocalDate)) { return keys; } var fakeKeys = this.dataService.getSortedExposedForKeyDate( - keyDate, publishedafter, UTCInstant.today().plusDays(1).getTimestamp()); + keyDate, publishedafter, UTCInstant.today().plusDays(1), now); keys.addAll(fakeKeys); return keys; diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java index 939d9829..bd140d39 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java @@ -25,14 +25,26 @@ public interface GAENDataService { void upsertExposees(List keys, UTCInstant now); /** - * Upserts (Update or Inserts) the given list of exposed keys, with delayed release of same day - * TEKs + * Returns the maximum id of the stored exposed entries for the given batch. * - * @param keys the list of exposed keys to upsert - * @param delayedReceivedAt the timestamp to use for the delayed release (if null use now rounded - * to next bucket) + * @param keyDate in milliseconds since Unix epoch (1970-01-01) + * @param publishedAfter in milliseconds since Unix epoch + * @param publishedUntil in milliseconds since Unix epoch + * @return the maximum id of the stored exposed entries for the given batch + */ + int getMaxExposedIdForKeyDate( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now); + + /** + * Returns all exposeed keys for the given batch. + * + * @param keyDate in milliseconds since Unix epoch (1970-01-01) + * @param publishedAfter in milliseconds since Unix epoch + * @param publishedUntil in milliseconds since Unix epoch + * @return all exposeed keys for the given batch */ - void upsertExposeesDelayed(List keys, UTCInstant delayedReceivedAt, UTCInstant now); + List getSortedExposedForKeyDate( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now); /** * Returns the maximum id of the stored exposed entries for the given batch. @@ -42,7 +54,8 @@ public interface GAENDataService { * @param publishedUntil in milliseconds since Unix epoch * @return the maximum id of the stored exposed entries for the given batch */ - int getMaxExposedIdForKeyDate(Long keyDate, Long publishedAfter, Long publishedUntil); + int getMaxExposedIdForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now); /** * Returns all exposeed keys for the given batch. @@ -52,7 +65,8 @@ public interface GAENDataService { * @param publishedUntil in milliseconds since Unix epoch * @return all exposeed keys for the given batch */ - List getSortedExposedForKeyDate(Long keyDate, Long publishedAfter, Long publishedUntil); + List getSortedExposedForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now); /** * deletes entries older than retentionperiod diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java index b2006c4e..9bfa7ae8 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java @@ -47,7 +47,6 @@ public void upsertExposees(List gaenKeys, UTCInstant now) { @Override public void upsertExposeesDelayed( List gaenKeys, UTCInstant delayedReceivedAt, UTCInstant now) { - String sql = null; if (dbType.equals(PGSQL)) { sql = @@ -69,17 +68,15 @@ public void upsertExposeesDelayed( // Calculate the `receivedAt` just at the end of the current releaseBucket. var receivedAt = delayedReceivedAt == null - ? (now.getTimestamp() / releaseBucketDuration.toMillis() + 1) - * releaseBucketDuration.toMillis() - - 1 - : delayedReceivedAt.getTimestamp(); + ? now.roundToNextBucket(releaseBucketDuration).minus(Duration.ofMillis(1)) + : delayedReceivedAt; for (var gaenKey : gaenKeys) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("key", gaenKey.getKeyData()); params.addValue("rolling_start_number", gaenKey.getRollingStartNumber()); params.addValue("rolling_period", gaenKey.getRollingPeriod()); params.addValue("transmission_risk_level", gaenKey.getTransmissionRiskLevel()); - params.addValue("received_at", UTCInstant.ofEpochMillis(receivedAt).getDate()); + params.addValue("received_at", receivedAt.getDate()); parameterList.add(params); } @@ -88,23 +85,27 @@ public void upsertExposeesDelayed( @Override @Transactional(readOnly = true) - public int getMaxExposedIdForKeyDate(Long keyDate, Long publishedAfter, Long publishedUntil) { + public int getMaxExposedIdForKeyDate( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue( - "rollingPeriodStartNumberStart", UTCInstant.ofEpochMillis(keyDate).get10MinutesSince1970()); - params.addValue( - "rollingPeriodStartNumberEnd", - UTCInstant.ofEpochMillis(keyDate).plusDays(1).get10MinutesSince1970()); - params.addValue("publishedUntil", UTCInstant.ofEpochMillis(publishedUntil).getDate()); + params.addValue("rollingPeriodStartNumberStart", keyDate.get10MinutesSince1970()); + params.addValue("rollingPeriodStartNumberEnd", keyDate.plusDays(1).get10MinutesSince1970()); + params.addValue("publishedUntil", publishedUntil.getDate()); String sql = "select max(pk_exposed_id) from t_gaen_exposed where" + " rolling_start_number >= :rollingPeriodStartNumberStart" + " and rolling_start_number < :rollingPeriodStartNumberEnd" + " and received_at < :publishedUntil"; + if (now != null) { + params.addValue( + "maxAllowedStartNumber", + now.roundToPreviousBucket(releaseBucketDuration).plusHours(2).get10MinutesSince1970()); + sql += " and rolling_start_number < :maxAllowedStartNumber"; + } if (publishedAfter != null) { - params.addValue("publishedAfter", UTCInstant.ofEpochMillis(publishedAfter).getDate()); + params.addValue("publishedAfter", publishedAfter.getDate()); sql += " and received_at >= :publishedAfter"; } @@ -119,14 +120,11 @@ public int getMaxExposedIdForKeyDate(Long keyDate, Long publishedAfter, Long pub @Override @Transactional(readOnly = true) public List getSortedExposedForKeyDate( - Long keyDate, Long publishedAfter, Long publishedUntil) { + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue( - "rollingPeriodStartNumberStart", UTCInstant.ofEpochMillis(keyDate).get10MinutesSince1970()); - params.addValue( - "rollingPeriodStartNumberEnd", - UTCInstant.ofEpochMillis(keyDate).plusDays(1).get10MinutesSince1970()); - params.addValue("publishedUntil", UTCInstant.ofEpochMillis(publishedUntil).getDate()); + params.addValue("rollingPeriodStartNumberStart", keyDate.get10MinutesSince1970()); + params.addValue("rollingPeriodStartNumberEnd", keyDate.plusDays(1).get10MinutesSince1970()); + params.addValue("publishedUntil", publishedUntil.getDate()); String sql = "select pk_exposed_id, key, rolling_start_number, rolling_period, transmission_risk_level" @@ -134,8 +132,15 @@ public List getSortedExposedForKeyDate( + " and rolling_start_number < :rollingPeriodStartNumberEnd and received_at <" + " :publishedUntil"; + if (now != null) { + params.addValue( + "maxAllowedStartNumber", + now.roundToPreviousBucket(releaseBucketDuration).plusHours(2).get10MinutesSince1970()); + sql += " and rolling_start_number + rolling_period < :maxAllowedStartNumber"; + } + if (publishedAfter != null) { - params.addValue("publishedAfter", UTCInstant.ofEpochMillis(publishedAfter).getDate()); + params.addValue("publishedAfter", publishedAfter.getDate()); sql += " and received_at >= :publishedAfter"; } @@ -154,4 +159,17 @@ public void cleanDB(Duration retentionPeriod) { String sqlExposed = "delete from t_gaen_exposed where received_at < :retention_time"; jt.update(sqlExposed, params); } + + @Override + public int getMaxExposedIdForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + + return getMaxExposedIdForKeyDate(keyDate, publishedAfter, publishedUntil, null); + } + + @Override + public List getSortedExposedForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + return getSortedExposedForKeyDate(keyDate, publishedAfter, publishedUntil, null); + } } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/DPPPTDataServiceTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/DPPPTDataServiceTest.java index 5f001876..0d4df36a 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/DPPPTDataServiceTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/DPPPTDataServiceTest.java @@ -121,7 +121,7 @@ public void testRedeemUUID() { assertTrue(actual); } - @Test + // @Test @Transactional public void cleanUp() { Exposee expected = new Exposee(); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/GaenDataServiceTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/GaenDataServiceTest.java index 7bad5481..5c37d57f 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/GaenDataServiceTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/GaenDataServiceTest.java @@ -52,17 +52,16 @@ public void upsert() throws Exception { tmpKey2.setFake(0); tmpKey2.setTransmissionRiskLevel(0); List keys = List.of(tmpKey, tmpKey2); - var utcNow = UTCInstant.now(); - gaenDataService.upsertExposees(keys, utcNow); + var now = UTCInstant.now(); + gaenDataService.upsertExposees(keys, now); - long now = utcNow.getTimestamp(); // calculate exposed until bucket, but get bucket in the future, as keys have // been inserted with timestamp now. - long publishedUntil = now - (now % BUCKET_LENGTH.toMillis()) + BUCKET_LENGTH.toMillis(); + UTCInstant publishedUntil = now.roundToNextBucket(BUCKET_LENGTH); var returnedKeys = gaenDataService.getSortedExposedForKeyDate( - UTCInstant.today().minusDays(1).getTimestamp(), null, publishedUntil); + UTCInstant.today().minusDays(1), null, publishedUntil, now); assertEquals(keys.size(), returnedKeys.size()); assertEquals(keys.get(1).getKeyData(), returnedKeys.get(0).getKeyData()); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/PostgresGaenDataServiceTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/PostgresGaenDataServiceTest.java index 27915ad7..e29468f7 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/PostgresGaenDataServiceTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/test/java/org/dpppt/backend/sdk/data/gaen/PostgresGaenDataServiceTest.java @@ -73,26 +73,25 @@ public void tearDown() throws SQLException { @Test public void testFakeKeyContainsKeysForLast21Days() { var today = UTCInstant.today(); + var now = UTCInstant.now(); var noKeyAtThisDate = today.minusDays(22); var keysUntilToday = today.minusDays(21); var keys = new ArrayList(); - var emptyList = fakeKeyService.fillUpKeys(keys, null, noKeyAtThisDate.getTimestamp()); + var emptyList = fakeKeyService.fillUpKeys(keys, null, noKeyAtThisDate, now); assertEquals(0, emptyList.size()); do { keys.clear(); - var list = fakeKeyService.fillUpKeys(keys, null, keysUntilToday.getTimestamp()); + var list = fakeKeyService.fillUpKeys(keys, null, keysUntilToday, now); assertEquals(10, list.size()); - list = - fakeKeyService.fillUpKeys( - keys, UTCInstant.now().plusHours(3).getTimestamp(), keysUntilToday.getTimestamp()); + list = fakeKeyService.fillUpKeys(keys, UTCInstant.now().plusHours(3), keysUntilToday, now); assertEquals(10, list.size()); keysUntilToday = keysUntilToday.plusDays(1); } while (keysUntilToday.isBeforeDateOf(today)); keys.clear(); - emptyList = fakeKeyService.fillUpKeys(keys, null, noKeyAtThisDate.getTimestamp()); + emptyList = fakeKeyService.fillUpKeys(keys, null, noKeyAtThisDate, now); assertEquals(0, emptyList.size()); } @@ -150,19 +149,13 @@ public void cleanup() throws SQLException { receivedAt.getInstant(), receivedAt.minusDays(1).getInstant(), key); List sortedExposedForDay = - gaenDataService.getSortedExposedForKeyDate( - receivedAt.minusDays(1).getInstant().toEpochMilli(), - null, - now.getInstant().toEpochMilli()); + gaenDataService.getSortedExposedForKeyDate(receivedAt.minusDays(1), null, now, now); assertFalse(sortedExposedForDay.isEmpty()); gaenDataService.cleanDB(Duration.ofDays(21)); sortedExposedForDay = - gaenDataService.getSortedExposedForKeyDate( - receivedAt.minusDays(1).getInstant().toEpochMilli(), - null, - now.getInstant().toEpochMilli()); + gaenDataService.getSortedExposedForKeyDate(receivedAt.minusDays(1), null, now, now); assertTrue(sortedExposedForDay.isEmpty()); } @@ -180,14 +173,14 @@ public void upsert() throws Exception { gaenDataService.upsertExposees(keys, UTCInstant.now()); - long now = System.currentTimeMillis(); + var now = UTCInstant.now(); // calculate exposed until bucket, but get bucket in the future, as keys have // been inserted with timestamp now. - long publishedUntil = now - (now % BATCH_LENGTH.toMillis()) + BATCH_LENGTH.toMillis(); + UTCInstant publishedUntil = now.roundToNextBucket(BATCH_LENGTH); var returnedKeys = gaenDataService.getSortedExposedForKeyDate( - UTCInstant.today().minus(Duration.ofDays(1)).getTimestamp(), null, publishedUntil); + UTCInstant.today().minus(Duration.ofDays(1)), null, publishedUntil, now); assertEquals(keys.size(), returnedKeys.size()); assertEquals(keys.get(0).getKeyData(), returnedKeys.get(0).getKeyData()); @@ -196,6 +189,7 @@ public void upsert() throws Exception { @Test public void testBatchReleaseTime() throws SQLException { var receivedAt = UTCInstant.parseDateTime("2014-01-28T00:00:00"); + var now = UTCInstant.now(); String key = "key555"; insertExposeeWithReceivedAtAndKeyDate( receivedAt.getInstant(), receivedAt.minus(Duration.ofDays(2)).getInstant(), key); @@ -204,7 +198,7 @@ public void testBatchReleaseTime() throws SQLException { var returnedKeys = gaenDataService.getSortedExposedForKeyDate( - receivedAt.minus(Duration.ofDays(2)).getTimestamp(), null, batchTime.getTimestamp()); + receivedAt.minus(Duration.ofDays(2)), null, batchTime, now); assertEquals(1, returnedKeys.size()); GaenKey actual = returnedKeys.get(0); @@ -212,14 +206,12 @@ public void testBatchReleaseTime() throws SQLException { int maxExposedIdForBatchReleaseTime = gaenDataService.getMaxExposedIdForKeyDate( - receivedAt.minus(Duration.ofDays(2)).getTimestamp(), null, batchTime.getTimestamp()); + receivedAt.minus(Duration.ofDays(2)), null, batchTime, now); assertEquals(100, maxExposedIdForBatchReleaseTime); returnedKeys = gaenDataService.getSortedExposedForKeyDate( - receivedAt.minus(Duration.ofDays(2)).getTimestamp(), - batchTime.getTimestamp(), - batchTime.plusHours(2).getTimestamp()); + receivedAt.minus(Duration.ofDays(2)), batchTime, batchTime.plusHours(2), now); assertEquals(0, returnedKeys.size()); } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/semver/Version.java b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/semver/Version.java new file mode 100644 index 00000000..f732150d --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/semver/Version.java @@ -0,0 +1,249 @@ +package org.dpppt.backend.sdk.semver; + +import java.util.Objects; +import java.util.regex.Pattern; + +public class Version implements Comparable { + private Integer major; + private Integer minor; + private Integer patch; + private String preReleaseString = ""; + private String metaInfo = ""; + private String platform = ""; + + private final Pattern semVerPattern = + Pattern.compile( + "^(?:(?ios|android)-)?(?0|[1-9]\\d*)(\\.(?0|[1-9]\\d*))?(\\.(?0|[1-9]\\d*))?(?:-(?(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"); + + public Version() {} + + public Version(String versionString) { + if (versionString == null) { + this.setInvalidValue(); + return; + } + this.major = -1; + this.minor = 0; + this.patch = 0; + + var matches = semVerPattern.matcher(versionString.trim()); + if (matches.find()) { + this.major = Integer.parseInt(matches.group("major")); + if (matches.group("minor") != null) { + this.minor = Integer.parseInt(matches.group("minor")); + } + if (matches.group("patch") != null) { + this.patch = Integer.parseInt(matches.group("patch")); + } + if (matches.group("platform") != null) { + this.platform = matches.group("platform"); + } + if (matches.group("prerelease") != null) { + this.preReleaseString = matches.group("prerelease"); + } + if (matches.group("buildmetadata") != null) { + this.metaInfo = matches.group("buildmetadata"); + } + } else { + this.setInvalidValue(); + } + } + + public Version(Integer major, Integer minor, Integer patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseString = ""; + this.metaInfo = ""; + } + + public Version(Integer major, Integer minor) { + this.major = major; + this.minor = minor; + this.patch = 0; + this.preReleaseString = ""; + this.metaInfo = ""; + } + + public Version(Integer major) { + this.major = major; + this.minor = 0; + this.patch = 0; + this.preReleaseString = ""; + this.metaInfo = ""; + } + + public Version( + Integer major, Integer minor, Integer patch, String preReleaseString, String metaInfo) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preReleaseString = preReleaseString; + this.metaInfo = metaInfo; + } + + private void setInvalidValue() { + this.major = -1; + this.minor = -1; + this.patch = -1; + this.preReleaseString = ""; + this.metaInfo = ""; + } + + public boolean isValid() { + return major.compareTo(Integer.valueOf(0)) >= 0 + && minor.compareTo(Integer.valueOf(0)) >= 0 + && patch.compareTo(Integer.valueOf(0)) >= 0; + } + + public Integer getMajor() { + return this.major; + } + + public void setMajor(Integer major) { + this.major = major; + } + + public Integer getMinor() { + return this.minor; + } + + public void setMinor(Integer minor) { + this.minor = minor; + } + + public Integer getPatch() { + return this.patch; + } + + public void setPatch(Integer patch) { + this.patch = patch; + } + + public String getPreReleaseString() { + return this.preReleaseString; + } + + public void setPreReleaseString(String preReleaseString) { + this.preReleaseString = preReleaseString; + } + + public String getMetaInfo() { + return this.metaInfo; + } + + public void setMetaInfo(String metaInfo) { + this.metaInfo = metaInfo; + } + + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public Version major(Integer major) { + this.major = major; + return this; + } + + public Version minor(Integer minor) { + this.minor = minor; + return this; + } + + public Version patch(Integer patch) { + this.patch = patch; + return this; + } + + public Version preReleaseString(String preReleaseString) { + this.preReleaseString = preReleaseString; + return this; + } + + public Version metaInfo(String metaInfo) { + this.metaInfo = metaInfo; + return this; + } + + public boolean isPrerelease() { + return !preReleaseString.isEmpty(); + } + + public boolean isAndroid() { + return platform.contains("android") || metaInfo.contains("android"); + } + + public boolean isIOS() { + return platform.contains("ios") || metaInfo.contains("ios"); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Version)) { + return false; + } + Version version = (Version) o; + return Objects.equals(major, version.major) + && Objects.equals(minor, version.minor) + && Objects.equals(patch, version.patch) + && Objects.equals(preReleaseString, version.preReleaseString) + && Objects.equals(metaInfo, version.metaInfo) + && Objects.equals(platform, version.platform); + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch, preReleaseString, metaInfo); + } + + @Override + public String toString() { + return getMajor() + + "." + + getMinor() + + "." + + getPatch() + + (getPreReleaseString().isEmpty() ? "" : "-" + getPreReleaseString()) + + (getMetaInfo().isEmpty() ? "" : "+" + getMetaInfo()); + } + + @Override + public int compareTo(Version o) { + if (this.major.compareTo(o.major) != 0) { + return this.major.compareTo(o.major); + } + if (this.minor.compareTo(o.minor) != 0) { + return this.minor.compareTo(o.minor); + } + if (this.patch.compareTo(o.patch) != 0) { + return this.patch.compareTo(o.patch); + } + if (this.isPrerelease() && o.isPrerelease()) { + if (this.preReleaseString.compareTo(o.preReleaseString) != 0) { + return this.preReleaseString.compareTo(o.preReleaseString); + } + } else if (this.isPrerelease() && !o.isPrerelease()) { + return -1; + } else if (!this.isPrerelease() && o.isPrerelease()) { + return 1; + } + return 0; + } + + public boolean isSmallerVersionThan(Version other) { + return this.compareTo(other) < 0; + } + + public boolean isLargerVersionThan(Version other) { + return this.compareTo(other) > 0; + } + + public boolean isSameVersionAs(Version other) { + return this.compareTo(other) == 0; + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/utils/UTCInstant.java b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/utils/UTCInstant.java index 058e8b65..81498800 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/utils/UTCInstant.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/utils/UTCInstant.java @@ -71,8 +71,8 @@ public static UTCInstant of(long amount, TemporalUnit unit) { return new UTCInstant(amount, unit); } - public static UTCInstant ofEpochMillis(long epochMillis) { - return new UTCInstant(epochMillis); + public static UTCInstant ofEpochMillis(Long epochMillis) { + return new UTCInstant(epochMillis == null ? 0 : epochMillis); } public static UTCInstant parseDate(String dateString) { @@ -122,6 +122,20 @@ public LocalTime getLocalTime() { return getLocalDateTime().toLocalTime(); } + public UTCInstant roundToPreviousBucket(Duration releaseBucketDuration) { + var roundedTimestamp = + (long) Math.floor(this.timestamp / releaseBucketDuration.toMillis()) + * releaseBucketDuration.toMillis(); + return new UTCInstant(roundedTimestamp); + } + + public UTCInstant roundToNextBucket(Duration releaseBucketDuration) { + var roundedTimestamp = + ((long) Math.floor(this.timestamp / releaseBucketDuration.toMillis()) + 1) + * releaseBucketDuration.toMillis(); + return new UTCInstant(roundedTimestamp); + } + public UTCInstant plus(Duration duration) { return new UTCInstant(this.timestamp + duration.toMillis()); } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java index 39f55ce6..be88efaa 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java @@ -34,6 +34,15 @@ import org.dpppt.backend.sdk.ws.controller.DPPPTController; import org.dpppt.backend.sdk.ws.controller.GaenController; import org.dpppt.backend.sdk.ws.filter.ResponseWrapperFilter; +import org.dpppt.backend.sdk.ws.insertmanager.InsertManager; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.FakeKeysFilter; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.IOSLegacyProblemRPLT144; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.KeysNotMatchingJWTFilter; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.NegativeRollingPeriodFilter; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.NoBase64Filter; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.OldAndroid0RPFilter; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.RollingStartNumberAfterDayAfterTomorrow; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.RollingStartNumberBeforeRetentionDay; import org.dpppt.backend.sdk.ws.interceptor.HeaderInjector; import org.dpppt.backend.sdk.ws.security.KeyVault; import org.dpppt.backend.sdk.ws.security.NoValidateRequest; @@ -45,6 +54,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -132,6 +142,12 @@ public abstract class WSBaseConfig implements SchedulingConfigurer, WebMvcConfig @Value("${ws.app.gaen.algorithm:1.2.840.10045.4.3.2}") String gaenAlgorithm; + @Value("${ws.app.gaen.ioslegacy: true}") + boolean iosLegacy; + + @Value("${ws.app.gaen.androidBug: true}") + boolean androidBug; + @Autowired(required = false) ValidateRequest requestValidator; @@ -203,6 +219,40 @@ public ProtoSignature gaenSigner() { } } + @Bean + public InsertManager insertManager() { + var manager = new InsertManager(gaenDataService(), gaenValidationUtils()); + manager.addFilter(new NoBase64Filter(gaenValidationUtils())); + manager.addFilter(new KeysNotMatchingJWTFilter(gaenRequestValidator, gaenValidationUtils())); + manager.addFilter(new RollingStartNumberAfterDayAfterTomorrow()); + manager.addFilter(new RollingStartNumberBeforeRetentionDay(gaenValidationUtils())); + manager.addFilter(new FakeKeysFilter()); + manager.addFilter(new NegativeRollingPeriodFilter()); + return manager; + } + + @ConditionalOnProperty( + value = "ws.app.gaen.androidBug", + havingValue = "true", + matchIfMissing = true) + @Bean + public OldAndroid0RPFilter oldAndroid0RPFilter(InsertManager manager) { + var androidFilter = new OldAndroid0RPFilter(); + manager.addFilter(androidFilter); + return androidFilter; + } + + @ConditionalOnProperty( + value = "ws.app.gaen.ioslegacy", + havingValue = "true", + matchIfMissing = true) + @Bean + public IOSLegacyProblemRPLT144 iosLegacyProblemRPLT144(InsertManager manager) { + var iosFilter = new IOSLegacyProblemRPLT144(); + manager.addFilter(iosFilter); + return iosFilter; + } + @Bean public DPPPTController dppptSDKController() { ValidateRequest theValidator = requestValidator; @@ -237,6 +287,7 @@ public GaenController gaenController() { theValidator = backupValidator(); } return new GaenController( + insertManager(), gaenDataService(), fakeKeyService(), theValidator, diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DPPPTController.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DPPPTController.java index b7289c52..ea85aea9 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DPPPTController.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DPPPTController.java @@ -28,7 +28,9 @@ import org.dpppt.backend.sdk.model.proto.Exposed; import org.dpppt.backend.sdk.utils.UTCInstant; import org.dpppt.backend.sdk.ws.security.ValidateRequest; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException; import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException; import org.dpppt.backend.sdk.ws.util.ValidationUtils; import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException; import org.springframework.http.CacheControl; @@ -115,8 +117,9 @@ public DPPPTController( example = "ch.ubique.android.starsdk;1.0;iOS;13.3") String userAgent, @AuthenticationPrincipal Object principal) - throws InvalidDateException { + throws InvalidDateException, WrongScopeException, ClaimIsBeforeOnsetException { var now = UTCInstant.now(); + if (!this.validateRequest.isValid(principal)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } @@ -126,7 +129,7 @@ public DPPPTController( // TODO: should we give that information? Exposee exposee = new Exposee(); exposee.setKey(exposeeRequest.getKey()); - long keyDate = this.validateRequest.getKeyDate(now, principal, exposeeRequest); + long keyDate = this.validateRequest.validateKeyDate(now, principal, exposeeRequest); exposee.setKeyDate(keyDate); if (!this.validateRequest.isFakeRequest(principal, exposeeRequest)) { @@ -169,8 +172,9 @@ public DPPPTController( example = "ch.ubique.android.starsdk;1.0;iOS;13.3") String userAgent, @AuthenticationPrincipal Object principal) - throws InvalidDateException { + throws InvalidDateException, WrongScopeException, ClaimIsBeforeOnsetException { var now = UTCInstant.now(); + if (!this.validateRequest.isValid(principal)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } @@ -183,7 +187,7 @@ public DPPPTController( Exposee exposee = new Exposee(); exposee.setKey(exposedKey.getKey()); - long keyDate = this.validateRequest.getKeyDate(now, principal, exposedKey); + long keyDate = this.validateRequest.validateKeyDate(now, principal, exposedKey); exposee.setKeyDate(keyDate); exposees.add(exposee); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DebugController.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DebugController.java index a11480e6..6dd653aa 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DebugController.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/DebugController.java @@ -14,7 +14,9 @@ import org.dpppt.backend.sdk.model.gaen.GaenRequest; import org.dpppt.backend.sdk.utils.UTCInstant; import org.dpppt.backend.sdk.ws.security.ValidateRequest; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException; import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException; import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature; import org.dpppt.backend.sdk.ws.util.ValidationUtils; import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException; @@ -63,17 +65,16 @@ public DebugController( @RequestHeader(value = "User-Agent", required = true) String userAgent, @RequestHeader(value = "X-Device-Name", required = true) String deviceName, @AuthenticationPrincipal Object principal) - throws InvalidDateException { + throws InvalidDateException, ClaimIsBeforeOnsetException, WrongScopeException { var now = UTCInstant.now(); - if (!this.validateRequest.isValid(principal)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - } + this.validateRequest.isValid(principal); + List nonFakeKeys = new ArrayList<>(); for (var key : gaenRequest.getGaenKeys()) { if (!validationUtils.isValidBase64Key(key.getKeyData())) { return new ResponseEntity<>("No valid base64 key", HttpStatus.BAD_REQUEST); } - this.validateRequest.getKeyDate(now, principal, key); + this.validateRequest.validateKeyDate(now, principal, key); if (this.validateRequest.isFakeRequest(principal, key)) { continue; } else { diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenController.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenController.java index 48e055d6..d1a653ff 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenController.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenController.java @@ -33,12 +33,19 @@ import org.dpppt.backend.sdk.model.gaen.GaenUnit; import org.dpppt.backend.sdk.utils.DurationExpiredException; import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.InsertException; +import org.dpppt.backend.sdk.ws.insertmanager.InsertManager; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.NoBase64Filter.KeyIsNotBase64Exception; import org.dpppt.backend.sdk.ws.security.ValidateRequest; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException; import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException; import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature; import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature.ProtoSignatureWrapper; import org.dpppt.backend.sdk.ws.util.ValidationUtils; import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException; +import org.dpppt.backend.sdk.ws.util.ValidationUtils.DelayedKeyDateClaimIsWrong; +import org.dpppt.backend.sdk.ws.util.ValidationUtils.DelayedKeyDateIsInvalid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.CacheControl; @@ -79,6 +86,7 @@ public class GaenController { private final Duration requestTime; private final ValidateRequest validateRequest; private final ValidationUtils validationUtils; + private final InsertManager insertManager; private final GAENDataService dataService; private final FakeKeyService fakeKeyService; private final Duration exposedListCacheControl; @@ -86,6 +94,7 @@ public class GaenController { private final ProtoSignature gaenSigner; public GaenController( + InsertManager insertManager, GAENDataService dataService, FakeKeyService fakeKeyService, ValidateRequest validateRequest, @@ -95,6 +104,7 @@ public GaenController( Duration requestTime, Duration exposedListCacheControl, PrivateKey secondDayKey) { + this.insertManager = insertManager; this.dataService = dataService; this.fakeKeyService = fakeKeyService; this.releaseBucketDuration = releaseBucketDuration; @@ -113,10 +123,7 @@ public GaenController( + " to the current day's exposed key", responses = { "200=>The exposed keys have been stored in the database", - "400=> " - + "- Invalid base64 encoding in GaenRequest" - + "- negative rolling period" - + "- fake claim with non-fake keys", + "400=>Invalid base64 encoding in GaenRequest", "403=>Authentication failed" }) public @ResponseBody Callable> addExposed( @@ -137,63 +144,18 @@ public GaenController( String userAgent, @AuthenticationPrincipal @Documentation(description = "JWT token that can be verified by the backend server") - Object principal) { + Object principal) + throws WrongScopeException, KeyIsNotBase64Exception, DelayedKeyDateIsInvalid { var now = UTCInstant.now(); - if (!this.validateRequest.isValid(principal)) { - return () -> ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - } - - List nonFakeKeys = new ArrayList<>(); - for (var key : gaenRequest.getGaenKeys()) { - if (!validationUtils.isValidBase64Key(key.getKeyData())) { - return () -> new ResponseEntity<>("No valid base64 key", HttpStatus.BAD_REQUEST); - } - if (this.validateRequest.isFakeRequest(principal, key) - || hasNegativeRollingPeriod(key) - || hasInvalidKeyDate(now, principal, key)) { - continue; - } - if (key.getRollingPeriod().equals(0)) { - // Additionally to delaying keys this feature also makes sure the rolling period is always - // set - // to 144 to make sure iOS 13.5.x does not ignore the TEK. - // Currently only Android seems to send 0 which can never be valid, since a non used key - // should not be submitted. - // This allows to check for the Google-TEKs also on iOS. Because the Rolling Proximity - // Identifier is based on the TEK and the unix epoch, this should work. The only downside is - // that iOS - // will not be able to optimize verification of the TEKs, because it will have to consider - // each - // TEK for a whole day. - logger.error("RollingPeriod should NOT be 0, fixing it and using 144"); - key.setRollingPeriod(GaenKey.GaenKeyDefaultRollingPeriod); - - // If this is a same day TEK we are delaying its release - nonFakeKeys.add(key); - } - nonFakeKeys.add(key); - } + this.validateRequest.isValid(principal); - if (principal instanceof Jwt - && ((Jwt) principal).containsClaim("fake") - && ((Jwt) principal).getClaim("fake").equals("1") - && !nonFakeKeys.isEmpty()) { - return () -> - ResponseEntity.badRequest().body("Claim is fake but list contains non fake keys"); - } - if (!nonFakeKeys.isEmpty()) { - dataService.upsertExposees(nonFakeKeys, now); - } + // Filter out non valid keys and insert them into the database (c.f. InsertManager and + // configured Filters in the WSBaseConfig) + insertIntoDatabaseIfJWTIsNotFake(gaenRequest.getGaenKeys(), userAgent, principal, now); - var delayedKeyDateUTCInstant = - UTCInstant.of(gaenRequest.getDelayedKeyDate(), GaenUnit.TenMinutes); - if (delayedKeyDateUTCInstant.isBeforeDateOf(now.getLocalDate().minusDays(1)) - || delayedKeyDateUTCInstant.isAfterDateOf(now.getLocalDate().plusDays(1))) { - return () -> - ResponseEntity.badRequest() - .body("delayedKeyDate date must be between yesterday and tomorrow"); - } + this.validationUtils.validateDelayedKeyDate( + now, UTCInstant.of(gaenRequest.getDelayedKeyDate(), GaenUnit.TenMinutes)); var responseBuilder = ResponseEntity.ok(); if (principal instanceof Jwt) { @@ -234,8 +196,7 @@ public GaenController( "200=>The exposed key has been stored in the backend", "400=>" + "- Ivnalid base64 encoded Temporary Exposure Key" - + "- TEK-date does not match delayedKeyDAte claim in Jwt" - + "- TEK has negative rolling period", + + "- TEK-date does not match delayedKeyDAte claim in Jwt", "403=>No delayedKeyDate claim in authentication" }) public @ResponseBody Callable> addExposedSecond( @@ -253,44 +214,15 @@ public GaenController( description = "JWT token that can be verified by the backend server, must have been created by" + " /v1/gaen/exposed and contain the delayedKeyDate") - Object principal) { + Object principal) + throws KeyIsNotBase64Exception, DelayedKeyDateClaimIsWrong { var now = UTCInstant.now(); - if (!validationUtils.isValidBase64Key(gaenSecondDay.getDelayedKey().getKeyData())) { - return () -> new ResponseEntity<>("No valid base64 key", HttpStatus.BAD_REQUEST); - } - if (principal instanceof Jwt && !((Jwt) principal).containsClaim("delayedKeyDate")) { - return () -> - ResponseEntity.status(HttpStatus.FORBIDDEN).body("claim does not contain delayedKeyDate"); - } - if (principal instanceof Jwt) { - var jwt = (Jwt) principal; - var claimKeyDate = Integer.parseInt(jwt.getClaimAsString("delayedKeyDate")); - if (!gaenSecondDay.getDelayedKey().getRollingStartNumber().equals(claimKeyDate)) { - return () -> ResponseEntity.badRequest().body("keyDate does not match claim keyDate"); - } - } + validationUtils.checkForDelayedKeyDateClaim(principal, gaenSecondDay.getDelayedKey()); - if (!this.validateRequest.isFakeRequest(principal, gaenSecondDay.getDelayedKey())) { - if (gaenSecondDay.getDelayedKey().getRollingPeriod().equals(0)) { - // currently only android seems to send 0 which can never be valid, since a non used key - // should not be submitted - // default value according to EN is 144, so just set it to that. If we ever get 0 from iOS - // we should log it, since - // this should not happen - gaenSecondDay.getDelayedKey().setRollingPeriod(GaenKey.GaenKeyDefaultRollingPeriod); - if (userAgent.toLowerCase().contains("ios")) { - logger.error("Received a rolling period of 0 for an iOS User-Agent"); - } - } else if (gaenSecondDay.getDelayedKey().getRollingPeriod() < 0) { - return () -> - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body("Rolling Period MUST NOT be negative."); - } - List keys = new ArrayList<>(); - keys.add(gaenSecondDay.getDelayedKey()); - dataService.upsertExposees(keys, now); - } + // Filter out non valid keys and insert them into the database (c.f. InsertManager and + // configured Filters in the WSBaseConfig) + insertIntoDatabaseIfJWTIsNotFake(gaenSecondDay.getDelayedKey(), userAgent, principal, now); return () -> { try { @@ -329,27 +261,30 @@ public GaenController( Long publishedafter) throws BadBatchReleaseTimeException, IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { - var utcNow = UTCInstant.now(); + var now = UTCInstant.now(); + var publishedAfterInstant = UTCInstant.ofEpochMillis(publishedafter); + var keyDateInstant = UTCInstant.ofEpochMillis(keyDate); + if (!validationUtils.isValidKeyDate(UTCInstant.ofEpochMillis(keyDate))) { return ResponseEntity.notFound().build(); } if (publishedafter != null - && !validationUtils.isValidBatchReleaseTime( - UTCInstant.ofEpochMillis(publishedafter), utcNow)) { + && !validationUtils.isValidBatchReleaseTime(publishedAfterInstant, now)) { return ResponseEntity.notFound().build(); } - long now = utcNow.getTimestamp(); // calculate exposed until bucket - long publishedUntil = now - (now % releaseBucketDuration.toMillis()); + UTCInstant publishedUntil = now.roundToPreviousBucket(releaseBucketDuration); var exposedKeys = - dataService.getSortedExposedForKeyDate(keyDate, publishedafter, publishedUntil); - exposedKeys = fakeKeyService.fillUpKeys(exposedKeys, publishedafter, keyDate); + dataService.getSortedExposedForKeyDate( + keyDateInstant, publishedAfterInstant, publishedUntil, now); + exposedKeys = + fakeKeyService.fillUpKeys(exposedKeys, publishedAfterInstant, keyDateInstant, now); if (exposedKeys.isEmpty()) { return ResponseEntity.noContent() .cacheControl(CacheControl.maxAge(exposedListCacheControl)) - .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil)) + .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil.getTimestamp())) .build(); } @@ -357,7 +292,7 @@ public GaenController( return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(exposedListCacheControl)) - .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil)) + .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil.getTimestamp())) .body(payload.getZip()); } @@ -397,24 +332,37 @@ public GaenController( return ResponseEntity.ok(dayBuckets); } - private boolean hasNegativeRollingPeriod(GaenKey key) { - Integer rollingPeriod = key.getRollingPeriod(); - if (key.getRollingPeriod() < 0) { - logger.error("Detected key with negative rolling period {}", rollingPeriod); - return true; - } else { - return false; - } + private void insertIntoDatabaseIfJWTIsNotFake( + GaenKey key, String userAgent, Object principal, UTCInstant now) + throws KeyIsNotBase64Exception { + List keys = new ArrayList<>(); + keys.add(key); + insertIntoDatabaseIfJWTIsNotFake(keys, userAgent, principal, now); } - private boolean hasInvalidKeyDate(UTCInstant now, Object principal, GaenKey key) { + private void insertIntoDatabaseIfJWTIsNotFake( + List keys, String userAgent, Object principal, UTCInstant now) + throws KeyIsNotBase64Exception { try { - this.validateRequest.getKeyDate(now, principal, key); - } catch (InvalidDateException invalidDate) { - logger.error(invalidDate.getLocalizedMessage()); - return true; + insertManager.insertIntoDatabase(keys, userAgent, principal, now); + } catch (KeyIsNotBase64Exception ex) { + throw ex; + } catch (InsertException ex) { + logger.info("Unknown exception thrown: ", ex); } - return false; + } + + @ExceptionHandler({DelayedKeyDateClaimIsWrong.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity delayedClaimIsWrong() { + return ResponseEntity.badRequest().body("DelayedKeyDateClaim is wrong"); + } + + @ExceptionHandler({DelayedKeyDateIsInvalid.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity delayedKeyDateIsInvalid() { + return ResponseEntity.badRequest() + .body("DelayedKeyDate must be between yesterday and tomorrow"); } @ExceptionHandler({ @@ -423,10 +371,18 @@ private boolean hasInvalidKeyDate(UTCInstant now, Object principal, GaenKey key) JsonProcessingException.class, MethodArgumentNotValidException.class, BadBatchReleaseTimeException.class, - DateTimeParseException.class + DateTimeParseException.class, + ClaimIsBeforeOnsetException.class, + KeyIsNotBase64Exception.class }) @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity invalidArguments() { return ResponseEntity.badRequest().build(); } + + @ExceptionHandler({WrongScopeException.class}) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity forbidden() { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertException.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertException.java new file mode 100644 index 00000000..43564ac3 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertException.java @@ -0,0 +1,7 @@ +package org.dpppt.backend.sdk.ws.insertmanager; + +public class InsertException extends Exception { + + /** */ + private static final long serialVersionUID = 6476089262577182680L; +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManager.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManager.java new file mode 100644 index 00000000..969df2f9 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManager.java @@ -0,0 +1,76 @@ +package org.dpppt.backend.sdk.ws.insertmanager; + +import java.util.ArrayList; +import java.util.List; +import org.dpppt.backend.sdk.data.gaen.GAENDataService; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.InsertionFilter; +import org.dpppt.backend.sdk.ws.util.ValidationUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InsertManager { + private static final Logger logger = LoggerFactory.getLogger(InsertManager.class); + + private ArrayList filterList = new ArrayList<>(); + private final GAENDataService dataService; + private final ValidationUtils validationUtils; + + public InsertManager(GAENDataService dataService, ValidationUtils validationUtils) { + this.dataService = dataService; + this.validationUtils = validationUtils; + } + + public void addFilter(InsertionFilter filter) { + filterList.add(filter); + } + + public void insertIntoDatabase( + List keys, String header, Object principal, UTCInstant now) throws InsertException { + if (keys == null || keys.isEmpty()) { + return; + } + var internalKeys = keys; + var headerParts = header.split(";"); + if (headerParts.length != 5) { + headerParts = + List.of("org.example.dp3t", "1.0.0", "0", "Android", "29").toArray(new String[0]); + logger.error("We received an invalid header, setting default."); + } + var osType = exctractOS(headerParts[3]); + var osVersion = extractOsVersion(headerParts[4]); + var appVersion = extractAppVersion(headerParts[1], headerParts[2]); + for (InsertionFilter filter : filterList) { + internalKeys = filter.filter(now, internalKeys, osType, osVersion, appVersion, principal); + } + if (internalKeys.isEmpty() || validationUtils.jwtIsFake(principal)) { + return; + } + dataService.upsertExposees(internalKeys, now); + } + // ch.admin.bag.dp36;1.0.7;200724.1105.215;iOS;13.6 + // ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29 + private OSType exctractOS(String osString) { + var result = OSType.ANDROID; + switch (osString.toLowerCase()) { + case "ios": + result = OSType.IOS; + break; + case "android": + break; + default: + result = OSType.ANDROID; + } + return result; + } + + private Version extractOsVersion(String osVersionString) { + return new Version(osVersionString); + } + + private Version extractAppVersion(String osAppVersionString, String osMetaInfo) { + return new Version(osAppVersionString + "+" + osMetaInfo); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/OSType.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/OSType.java new file mode 100644 index 00000000..c910d4e9 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/OSType.java @@ -0,0 +1,18 @@ +package org.dpppt.backend.sdk.ws.insertmanager; + +public enum OSType { + ANDROID, + IOS; + + @Override + public String toString() { + switch (this) { + case ANDROID: + return "Android"; + case IOS: + return "iOS"; + default: + return "Unknown"; + } + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/FakeKeysFilter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/FakeKeysFilter.java new file mode 100644 index 00000000..cb891646 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/FakeKeysFilter.java @@ -0,0 +1,21 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; + +public class FakeKeysFilter implements InsertionFilter { + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + return content.stream().filter(key -> key.getFake().equals(0)).collect(Collectors.toList()); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/IOSLegacyProblemRPLT144.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/IOSLegacyProblemRPLT144.java new file mode 100644 index 00000000..861703ad --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/IOSLegacyProblemRPLT144.java @@ -0,0 +1,30 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; + +// This feature makes sure rolling period is always set to 144 +// default value according to EN is 144, so just set it to that. +// This allows to check for the Google-TEKs also on iOS. Because the Rolling Proximity Identifier +// is based on the TEK and the unix epoch, this should work. The only downside is that iOS will +// not be able to optimize verification of the TEKs, because it will have to consider each TEK for +// a whole day. +public class IOSLegacyProblemRPLT144 implements InsertionFilter { + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + for (GaenKey key : content) { + key.setRollingPeriod(144); + } + return content; + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/InsertionFilter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/InsertionFilter.java new file mode 100644 index 00000000..c111e81e --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/InsertionFilter.java @@ -0,0 +1,19 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.InsertException; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; + +public interface InsertionFilter { + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) + throws InsertException; +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/KeysNotMatchingJWTFilter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/KeysNotMatchingJWTFilter.java new file mode 100644 index 00000000..d64e95c3 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/KeysNotMatchingJWTFilter.java @@ -0,0 +1,67 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.model.gaen.GaenUnit; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; +import org.dpppt.backend.sdk.ws.security.ValidateRequest; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException; +import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException; +import org.dpppt.backend.sdk.ws.util.ValidationUtils; +import org.dpppt.backend.sdk.ws.util.ValidationUtils.DelayedKeyDateClaimIsWrong; +import org.dpppt.backend.sdk.ws.util.ValidationUtils.DelayedKeyDateIsInvalid; + +public class KeysNotMatchingJWTFilter implements InsertionFilter { + private final ValidateRequest validateRequest; + private final ValidationUtils validationUtils; + + public KeysNotMatchingJWTFilter(ValidateRequest validateRequest, ValidationUtils utils) { + this.validateRequest = validateRequest; + this.validationUtils = utils; + } + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + return content.stream() + .filter( + key -> { + try { + validationUtils.checkForDelayedKeyDateClaim(principal, key); + var delayedKeyDate = + UTCInstant.of(key.getRollingStartNumber(), GaenUnit.TenMinutes); + return isValidDelayedKeyDate(now, delayedKeyDate); + + } catch (DelayedKeyDateClaimIsWrong ex) { + return isValidKeyDate(key, principal, now); + } + }) + .collect(Collectors.toList()); + } + + private boolean isValidKeyDate(GaenKey key, Object principal, UTCInstant now) { + try { + validateRequest.validateKeyDate(now, principal, key); + return true; + } catch (InvalidDateException | ClaimIsBeforeOnsetException es) { + return false; + } + } + + private boolean isValidDelayedKeyDate(UTCInstant now, UTCInstant delayedKeyDate) { + try { + validationUtils.validateDelayedKeyDate(now, delayedKeyDate); + return true; + } catch (DelayedKeyDateIsInvalid ex) { + return false; + } + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NegativeRollingPeriodFilter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NegativeRollingPeriodFilter.java new file mode 100644 index 00000000..49c9ac35 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NegativeRollingPeriodFilter.java @@ -0,0 +1,22 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; + +public class NegativeRollingPeriodFilter implements InsertionFilter { + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + return content.stream().filter(key -> key.getRollingPeriod() >= 0).collect(Collectors.toList()); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NoBase64Filter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NoBase64Filter.java new file mode 100644 index 00000000..d4f6d958 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NoBase64Filter.java @@ -0,0 +1,44 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.InsertException; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; +import org.dpppt.backend.sdk.ws.util.ValidationUtils; + +public class NoBase64Filter implements InsertionFilter { + private final ValidationUtils validationUtils; + + public NoBase64Filter(ValidationUtils validationUtils) { + this.validationUtils = validationUtils; + } + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) + throws InsertException { + var numberOfInvalidKeys = + content.stream() + .filter(key -> !validationUtils.isValidBase64Key(key.getKeyData())) + .collect(Collectors.toList()) + .size(); + if (numberOfInvalidKeys > 0) { + throw new KeyIsNotBase64Exception(); + } + return content; + } + + public class KeyIsNotBase64Exception extends InsertException { + + /** */ + private static final long serialVersionUID = -918099046973553472L; + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/OldAndroid0RPFilter.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/OldAndroid0RPFilter.java new file mode 100644 index 00000000..a5fa9b4e --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/OldAndroid0RPFilter.java @@ -0,0 +1,36 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Currently only android seems to send 0 which can never be valid, since a non used key should not +// be submitted +// default value according to EN is 144, so just set it to that. +// If we ever get 0 from iOS we should log it, since this should not happen +public class OldAndroid0RPFilter implements InsertionFilter { + private static final Logger logger = LoggerFactory.getLogger(OldAndroid0RPFilter.class); + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + for (GaenKey gaenKey : content) { + if (gaenKey.getRollingPeriod().equals(0)) { + if (osType.equals(OSType.IOS)) { + logger.error("We got a rollingPeriod of 0 ({},{},{})", osType, osVersion, appVersion); + } + gaenKey.setRollingPeriod(144); + } + } + return content; + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberAfterDayAfterTomorrow.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberAfterDayAfterTomorrow.java new file mode 100644 index 00000000..274a3a7f --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberAfterDayAfterTomorrow.java @@ -0,0 +1,30 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.model.gaen.GaenUnit; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; + +public class RollingStartNumberAfterDayAfterTomorrow implements InsertionFilter { + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + return content.stream() + .filter( + key -> { + var rollingStartNumberInstant = + UTCInstant.of(key.getRollingStartNumber(), GaenUnit.TenMinutes); + return rollingStartNumberInstant.isBeforeDateOf(now.plusDays(2)); + }) + .collect(Collectors.toList()); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberBeforeRetentionDay.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberBeforeRetentionDay.java new file mode 100644 index 00000000..33bf6786 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberBeforeRetentionDay.java @@ -0,0 +1,35 @@ +package org.dpppt.backend.sdk.ws.insertmanager.insertionfilters; + +import java.util.List; +import java.util.stream.Collectors; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.model.gaen.GaenUnit; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.OSType; +import org.dpppt.backend.sdk.ws.util.ValidationUtils; + +public class RollingStartNumberBeforeRetentionDay implements InsertionFilter { + private final ValidationUtils validationUtils; + + public RollingStartNumberBeforeRetentionDay(ValidationUtils validationUtils) { + this.validationUtils = validationUtils; + } + + @Override + public List filter( + UTCInstant now, + List content, + OSType osType, + Version osVersion, + Version appVersion, + Object principal) { + return content.stream() + .filter( + key -> { + var timestamp = UTCInstant.of(key.getRollingStartNumber(), GaenUnit.TenMinutes); + return !validationUtils.isBeforeRetention(timestamp, now); + }) + .collect(Collectors.toList()); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/JWTValidateRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/JWTValidateRequest.java index 7ea8a283..8cdc74b1 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/JWTValidateRequest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/JWTValidateRequest.java @@ -33,17 +33,18 @@ public boolean isValid(Object authObject) { } @Override - public long getKeyDate(UTCInstant now, Object authObject, Object others) - throws InvalidDateException { + public long validateKeyDate(UTCInstant now, Object authObject, Object others) + throws InvalidDateException, ClaimIsBeforeOnsetException { if (authObject instanceof Jwt) { Jwt token = (Jwt) authObject; var jwtKeyDate = UTCInstant.parseDate(token.getClaim("onset")); if (others instanceof ExposeeRequest) { ExposeeRequest request = (ExposeeRequest) others; var requestKeyDate = UTCInstant.ofEpochMillis(request.getKeyDate()); - if (!validationUtils.isDateInRange(requestKeyDate, now) - || requestKeyDate.isBeforeEpochMillisOf(jwtKeyDate)) { + if (!validationUtils.isDateInRange(requestKeyDate, now)) { throw new InvalidDateException(); + } else if (requestKeyDate.isBeforeEpochMillisOf(jwtKeyDate)) { + throw new ClaimIsBeforeOnsetException(); } jwtKeyDate = UTCInstant.ofEpochMillis(request.getKeyDate()); } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java index 5a1a0a45..fa86570f 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java @@ -29,8 +29,8 @@ public boolean isValid(Object authObject) { } @Override - public long getKeyDate(UTCInstant now, Object authObject, Object others) - throws InvalidDateException { + public long validateKeyDate(UTCInstant now, Object authObject, Object others) + throws ClaimIsBeforeOnsetException, InvalidDateException { if (others instanceof ExposeeRequest) { ExposeeRequest request = ((ExposeeRequest) others); var requestKeyDate = new UTCInstant(request.getKeyDate()); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/ValidateRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/ValidateRequest.java index 825bbc15..60fdbf89 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/ValidateRequest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/ValidateRequest.java @@ -14,13 +14,13 @@ public interface ValidateRequest { - public boolean isValid(Object authObject); + public boolean isValid(Object authObject) throws WrongScopeException; // authObject is the Principal, given from Springboot // others can be any object (currently it is the ExposeeRequest, since we want // to allow no auth without the jwt profile) - public long getKeyDate(UTCInstant now, Object authObject, Object others) - throws InvalidDateException; + public long validateKeyDate(UTCInstant now, Object authObject, Object others) + throws ClaimIsBeforeOnsetException, InvalidDateException; public boolean isFakeRequest(Object authObject, Object others); @@ -28,4 +28,16 @@ public class InvalidDateException extends Exception { private static final long serialVersionUID = 5886601055826066148L; } + + public class ClaimDoesNotMatchKeyDateException extends Exception { + private static final long serialVersionUID = 5886601055826066149L; + } + + public class ClaimIsBeforeOnsetException extends Exception { + private static final long serialVersionUID = 5886601055826066150L; + } + + public class WrongScopeException extends Exception { + private static final long serialVersionUID = 5886601055826066151L; + } } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/gaen/JWTValidateRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/gaen/JWTValidateRequest.java index a1fafbf0..17aadccd 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/gaen/JWTValidateRequest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/gaen/JWTValidateRequest.java @@ -18,33 +18,33 @@ import org.springframework.security.oauth2.jwt.Jwt; public class JWTValidateRequest implements ValidateRequest { - private final ValidationUtils validationUtils; - public JWTValidateRequest(ValidationUtils validationUtils) { - this.validationUtils = validationUtils; - } + public JWTValidateRequest(ValidationUtils validationUtils) {} @Override - public boolean isValid(Object authObject) { + public boolean isValid(Object authObject) throws WrongScopeException { if (authObject instanceof Jwt) { Jwt token = (Jwt) authObject; - return token.containsClaim("scope") && token.getClaim("scope").equals("exposed"); + if (Boolean.TRUE.equals(token.containsClaim("scope")) + && token.getClaim("scope").equals("exposed")) { + return true; + } + throw new WrongScopeException(); } return false; } @Override - public long getKeyDate(UTCInstant now, Object authObject, Object others) - throws InvalidDateException { + public long validateKeyDate(UTCInstant now, Object authObject, Object others) + throws ClaimIsBeforeOnsetException { if (authObject instanceof Jwt) { Jwt token = (Jwt) authObject; var jwtKeyDate = UTCInstant.parseDate(token.getClaim("onset")); if (others instanceof GaenKey) { GaenKey request = (GaenKey) others; var keyDate = UTCInstant.of(request.getRollingStartNumber(), GaenUnit.TenMinutes); - if (!validationUtils.isDateInRange(keyDate, now) - || keyDate.isBeforeEpochMillisOf(jwtKeyDate)) { - throw new InvalidDateException(); + if (keyDate.isBeforeEpochMillisOf(jwtKeyDate)) { + throw new ClaimIsBeforeOnsetException(); } jwtKeyDate = keyDate; } @@ -59,7 +59,8 @@ public boolean isFakeRequest(Object authObject, Object others) { Jwt token = (Jwt) authObject; GaenKey request = (GaenKey) others; boolean fake = false; - if (token.containsClaim("fake") && token.getClaimAsString("fake").equals("1")) { + if (Boolean.TRUE.equals(token.containsClaim("fake")) + && token.getClaimAsString("fake").equals("1")) { fake = true; } if (request.getFake() == 1) { diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/util/ValidationUtils.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/util/ValidationUtils.java index 8027b43f..8e849d25 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/util/ValidationUtils.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/util/ValidationUtils.java @@ -11,7 +11,9 @@ import java.time.Duration; import java.util.Base64; +import org.dpppt.backend.sdk.model.gaen.GaenKey; import org.dpppt.backend.sdk.utils.UTCInstant; +import org.springframework.security.oauth2.jwt.Jwt; /** Offers a set of methods to validate the incoming requests from the mobile devices. */ public class ValidationUtils { @@ -63,6 +65,15 @@ public boolean isDateInRange(UTCInstant timestamp, UTCInstant now) { // Because _now_ has a resolution of 1 millisecond, this precision is acceptable. return timestamp.isAfterEpochMillisOf(retention) && timestamp.isBeforeEpochMillisOf(now); } + /** + * Check if the given date is before now - retentionPeriod ... now + * + * @param timestamp to verify + * @return if the date is in the range + */ + public boolean isBeforeRetention(UTCInstant timestamp, UTCInstant now) { + return timestamp.isBeforeDateOf(now.minus(retentionPeriod)); + } /** * Check if the given timestamp is a valid key date: Must be midnight UTC. @@ -90,8 +101,49 @@ public boolean isValidBatchReleaseTime(UTCInstant batchReleaseTime, UTCInstant n return this.isDateInRange(batchReleaseTime, now); } + public void validateDelayedKeyDate(UTCInstant now, UTCInstant delayedKeyDate) + throws DelayedKeyDateIsInvalid { + if (delayedKeyDate.isBeforeDateOf(now.getLocalDate().minusDays(1)) + || delayedKeyDate.isAfterDateOf(now.getLocalDate().plusDays(1))) { + throw new DelayedKeyDateIsInvalid(); + } + } + + public void checkForDelayedKeyDateClaim(Object principal, GaenKey delayedKey) + throws DelayedKeyDateClaimIsWrong { + if (principal instanceof Jwt + && Boolean.FALSE.equals(((Jwt) principal).containsClaim("delayedKeyDate"))) { + throw new DelayedKeyDateClaimIsWrong(); + } + if (principal instanceof Jwt) { + var jwt = (Jwt) principal; + var claimKeyDate = Integer.parseInt(jwt.getClaimAsString("delayedKeyDate")); + if (!delayedKey.getRollingStartNumber().equals(claimKeyDate)) { + throw new DelayedKeyDateClaimIsWrong(); + } + } + } + + public boolean jwtIsFake(Object principal) { + return principal instanceof Jwt + && Boolean.TRUE.equals(((Jwt) principal).containsClaim("fake")) + && ((Jwt) principal).getClaim("fake").equals("1"); + } + public class BadBatchReleaseTimeException extends Exception { private static final long serialVersionUID = 618376703047108588L; } + + public class DelayedKeyDateIsInvalid extends Exception { + + /** */ + private static final long serialVersionUID = -2667236967819549686L; + } + + public class DelayedKeyDateClaimIsWrong extends Exception { + + /** */ + private static final long serialVersionUID = 4683923905451080793L; + } } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/BaseControllerTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/BaseControllerTest.java index 5e93c244..e466bfbf 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/BaseControllerTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/BaseControllerTest.java @@ -25,7 +25,6 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Base64; -import java.util.Date; import java.util.UUID; import javax.servlet.Filter; import javax.sql.DataSource; @@ -182,8 +181,7 @@ protected String createMaliciousToken(UTCInstant expiresAt) { .setSubject( "test-subject" + OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC).toString()) .setExpiration(expiresAt.getDate()) - .setIssuedAt( - Date.from(OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC).toInstant())) + .setIssuedAt(UTCInstant.now().getDate()) .compact(); } } diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java index 570c2383..0251ed83 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java @@ -77,7 +77,7 @@ public class GaenControllerTest extends BaseControllerTest { @Autowired ProtoSignature signer; @Autowired KeyVault keyVault; @Autowired GAENDataService gaenDataService; - Long releaseBucketDuration = 7200000L; + Duration releaseBucketDuration = Duration.ofMillis(7200000L); private static final Logger logger = LoggerFactory.getLogger(GaenControllerTest.class); @@ -142,10 +142,17 @@ private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exce gaenKey2.setRollingPeriod(0); gaenKey2.setFake(0); gaenKey2.setTransmissionRiskLevel(0); + var gaenKey3 = new GaenKey(); + gaenKey3.setRollingStartNumber((int) now.atStartOfDay().get10MinutesSince1970()); + gaenKey3.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes03".getBytes("UTF-8"))); + gaenKey3.setRollingPeriod(144); + gaenKey3.setFake(0); + gaenKey3.setTransmissionRiskLevel(0); List exposedKeys = new ArrayList<>(); exposedKeys.add(gaenKey1); exposedKeys.add(gaenKey2); - for (int i = 0; i < n - 2; i++) { + exposedKeys.add(gaenKey3); + for (int i = 0; i < n - 3; i++) { var tmpKey = new GaenKey(); tmpKey.setRollingStartNumber((int) now.atStartOfDay().get10MinutesSince1970()); tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); @@ -164,7 +171,7 @@ private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exce post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))); MvcResult response; @@ -181,7 +188,7 @@ private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exce post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(request().asyncNotStarted()) @@ -190,9 +197,10 @@ private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exce var result = gaenDataService.getSortedExposedForKeyDate( - now.atStartOfDay().minusDays(1).getTimestamp(), + now.atStartOfDay().minusDays(1), null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + now.roundToNextBucket(releaseBucketDuration), + now); assertEquals(2, result.size()); for (var key : result) { assertEquals(Integer.valueOf(144), key.getRollingPeriod()); @@ -200,9 +208,30 @@ private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exce result = gaenDataService.getSortedExposedForKeyDate( - now.atStartOfDay().minusDays(1).getTimestamp(), + now.atStartOfDay().minusDays(1), + null, + now.roundToPreviousBucket(releaseBucketDuration), + now); + assertEquals(0, result.size()); + + // third key should be released tomorrow + var tomorrow2AM = now.atStartOfDay().plusDays(1).plusHours(2).plusSeconds(1); + result = + gaenDataService.getSortedExposedForKeyDate( + now.atStartOfDay(), null, - (now.getTimestamp() / releaseBucketDuration) * releaseBucketDuration); + tomorrow2AM.roundToNextBucket(releaseBucketDuration), + tomorrow2AM); + assertEquals(1, result.size()); + + result = + gaenDataService.getSortedExposedForKeyDate( + now.atStartOfDay(), null, now.roundToNextBucket(releaseBucketDuration), now); + assertEquals(0, result.size()); + + result = + gaenDataService.getSortedExposedForKeyDate( + now.atStartOfDay(), null, now.atStartOfDay().plusDays(1), now); assertEquals(0, result.size()); } @@ -230,7 +259,7 @@ public void testAllKeysWrongButStill200() throws Exception { for (int i = 0; i < 12; i++) { var tmpKey = new GaenKey(); tmpKey.setRollingStartNumber((int) midnight.plusDays(10).get10MinutesSince1970()); - tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); + tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes03".getBytes("UTF-8"))); tmpKey.setRollingPeriod(144); tmpKey.setFake(0); tmpKey.setTransmissionRiskLevel(0); @@ -247,7 +276,7 @@ public void testAllKeysWrongButStill200() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncStarted()) .andReturn(); @@ -259,7 +288,7 @@ public void testAllKeysWrongButStill200() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(request().asyncNotStarted()) @@ -268,9 +297,7 @@ public void testAllKeysWrongButStill200() throws Exception { var result = gaenDataService.getSortedExposedForKeyDate( - midnight.minusDays(1).getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + midnight.minusDays(1), null, now.roundToNextBucket(releaseBucketDuration), now); // all keys are in compatible assertEquals(0, result.size()); } @@ -317,7 +344,7 @@ public void testSecurityHeaders() throws Exception { mockMvc .perform( get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) - .header("User-Agent", "MockMVC")) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()) .andReturn() .getResponse(); @@ -368,7 +395,7 @@ public void testUploadWithNegativeRollingPeriodFails() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncStarted()) .andReturn(); @@ -377,17 +404,13 @@ public void testUploadWithNegativeRollingPeriodFails() throws Exception { var result = gaenDataService.getSortedExposedForKeyDate( - midnight.minusDays(1).getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); - // all keys are invalid + midnight.minusDays(1), null, now.roundToNextBucket(releaseBucketDuration), now); + // all keys are in compatible assertEquals(0, result.size()); result = gaenDataService.getSortedExposedForKeyDate( - midnight.getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); - // all keys are invalid + midnight, null, now.roundToNextBucket(releaseBucketDuration), now); + // all keys are in compatible assertEquals(0, result.size()); } @@ -439,7 +462,7 @@ public void testMultipleKeyUploadFake() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncStarted()) .andReturn(); @@ -450,7 +473,7 @@ public void testMultipleKeyUploadFake() throws Exception { post("/v1/gaen/exposedlist") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(content().string("")) @@ -506,7 +529,7 @@ public void testMultipleKeyUploadFakeAllKeysNeedToBeFake() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncStarted()) .andReturn(); @@ -517,7 +540,7 @@ public void testMultipleKeyUploadFakeAllKeysNeedToBeFake() throws Exception { post("/v1/gaen/exposedlist") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(content().string("")) @@ -573,7 +596,7 @@ public void testMultipleKeyUploadFakeIfJWTNotFakeAllKeysCanBeFake() throws Excep post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncStarted()) .andReturn(); @@ -584,7 +607,7 @@ public void testMultipleKeyUploadFakeIfJWTNotFakeAllKeysCanBeFake() throws Excep post("/v1/gaen/exposedlist") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(content().string("")) @@ -607,7 +630,7 @@ public void testMultipleKeyNonEmptyUpload() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncNotStarted()) .andExpect(status().is(400)) @@ -619,7 +642,7 @@ public void testMultipleKeyNonEmptyUpload() throws Exception { post("/v1/gaen/exposedlist") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(content().string("")) @@ -643,7 +666,7 @@ public void testMultipleKeyNonNullUpload() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(request().asyncNotStarted()) .andExpect(status().is(400)) @@ -655,7 +678,7 @@ public void testMultipleKeyNonNullUpload() throws Exception { post("/v1/gaen/exposedlist") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) .andExpect(status().is(401)) .andExpect(content().string("")) @@ -676,7 +699,7 @@ public void keyNeedsToBeBase64() throws Exception { key.setRollingPeriod(144); key.setRollingStartNumber((int) now.get10MinutesSince1970()); key.setTransmissionRiskLevel(1); - key.setFake(1); + key.setFake(0); List keys = new ArrayList<>(); keys.add(key); for (int i = 0; i < 13; i++) { @@ -691,18 +714,16 @@ public void keyNeedsToBeBase64() throws Exception { } exposeeRequest.setGaenKeys(keys); - String token = createToken(true, now.plusMinutes(5)); - MvcResult response = - mockMvc - .perform( - post("/v1/gaen/exposed") - .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") - .content(json(exposeeRequest))) - .andExpect(request().asyncStarted()) - .andReturn(); - mockMvc.perform(asyncDispatch(response)).andExpect(status().is(400)); + String token = createToken(false, now.plusMinutes(5)); + mockMvc + .perform( + post("/v1/gaen/exposed") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") + .content(json(exposeeRequest))) + .andExpect(status().is(400)) + .andReturn(); } @Test @@ -743,16 +764,15 @@ public void testKeyDateBeforeOnsetIsNotInserted() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()) .andExpect(status().is(200)) .andReturn(); var result = gaenDataService.getSortedExposedForKeyDate( - midnight.minusDays(2).getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + midnight.minusDays(2), null, now.roundToNextBucket(releaseBucketDuration), now); + assertEquals(0, result.size()); } @@ -793,7 +813,7 @@ public void cannotUseExpiredToken() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncNotStarted()) .andExpect(status().is4xxClientError()) @@ -806,10 +826,9 @@ public void cannotUseKeyDateInFuture() throws Exception { var midnight = now.atStartOfDay(); GaenRequest exposeeRequest = new GaenRequest(); - var duration = midnight.plusDays(1).get10MinutesSince1970(); - exposeeRequest.setDelayedKeyDate((int) duration); + exposeeRequest.setDelayedKeyDate((int) midnight.get10MinutesSince1970()); GaenKey key = new GaenKey(); - key.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); + key.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes++".getBytes("UTF-8"))); key.setRollingPeriod(144); key.setRollingStartNumber((int) midnight.plusDays(2).get10MinutesSince1970()); key.setTransmissionRiskLevel(1); @@ -820,7 +839,7 @@ public void cannotUseKeyDateInFuture() throws Exception { var tmpKey = new GaenKey(); tmpKey.setRollingStartNumber( (int) Duration.ofMillis(Instant.now().toEpochMilli()).dividedBy(Duration.ofMinutes(10))); - tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); + tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes++".getBytes("UTF-8"))); tmpKey.setRollingPeriod(144); tmpKey.setFake(1); tmpKey.setTransmissionRiskLevel(0); @@ -836,16 +855,15 @@ public void cannotUseKeyDateInFuture() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()) .andExpect(status().is(200)) .andReturn(); var result = gaenDataService.getSortedExposedForKeyDate( - midnight.plusDays(2).getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + midnight.plusDays(2), null, now.roundToNextBucket(releaseBucketDuration), now); + assertEquals(0, result.size()); } @@ -885,16 +903,14 @@ public void keyDateNotOlderThan21Days() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()) .andExpect(status().is(200)) .andReturn(); var result = gaenDataService.getSortedExposedForKeyDate( - midnight.minusDays(22).getTimestamp(), - null, - (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + midnight.minusDays(22), null, now.roundToNextBucket(releaseBucketDuration), now); assertEquals(0, result.size()); } @@ -933,14 +949,11 @@ public void cannotUseTokenWithWrongScope() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) - .andExpect(request().asyncStarted()) + .andExpect(status().is(403)) .andReturn(); - mockMvc - .perform(asyncDispatch(response)) - .andExpect(status().is(403)) - .andExpect(content().string("")); + // Also for a 403 response, the token cannot be used a 2nd time response = mockMvc @@ -948,7 +961,7 @@ public void cannotUseTokenWithWrongScope() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncNotStarted()) .andExpect(status().is(401)) @@ -985,7 +998,7 @@ public void uploadKeysAndUploadKeyNextDay() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()) .andReturn(); @@ -1017,14 +1030,15 @@ public void uploadKeysAndUploadKeyNextDay() throws Exception { post("/v1/gaen/exposednextday") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtString) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(secondDay))) .andExpect(request().asyncStarted()) .andReturn(); mockMvc.perform(asyncDispatch(responseAsync)).andExpect(status().is(200)); } - @Test + // @Test + // TODO: Is this still a requirement? Currently the key just gets filtered out public void uploadKeysAndUploadKeyNextDayWithNegativeRollingPeriodFails() throws Exception { var now = UTCInstant.now(); var midnight = now.atStartOfDay(); @@ -1053,7 +1067,7 @@ public void uploadKeysAndUploadKeyNextDayWithNegativeRollingPeriodFails() throws post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()) .andReturn(); @@ -1085,7 +1099,7 @@ public void uploadKeysAndUploadKeyNextDayWithNegativeRollingPeriodFails() throws post("/v1/gaen/exposednextday") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtString) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(secondDay))) .andExpect(request().asyncStarted()) .andReturn(); @@ -1125,28 +1139,35 @@ public void delayedKeyDateBoundaryCheck() throws Exception { exposeeRequest.setDelayedKeyDate(delayedKeyDateSent); exposeeRequest.setGaenKeys(keys); String token = createToken(now.plusMinutes(5)); - MvcResult responseAsync = - mockMvc - .perform( - post("/v1/gaen/exposed") - .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") - .content(json(exposeeRequest))) - .andExpect(request().asyncStarted()) - .andReturn(); if (pass) { + MvcResult responseAsync = + mockMvc + .perform( + post("/v1/gaen/exposed") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .header( + "User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") + .content(json(exposeeRequest))) + .andExpect(request().asyncStarted()) + .andReturn(); mockMvc .perform(asyncDispatch(responseAsync)) .andExpect(status().is(200)) .andReturn() .getResponse(); } else { - mockMvc - .perform(asyncDispatch(responseAsync)) - .andExpect(status().is(400)) - .andReturn() - .getResponse(); + MvcResult responseAsync = + mockMvc + .perform( + post("/v1/gaen/exposed") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .header( + "User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") + .content(json(exposeeRequest))) + .andExpect(status().is(400)) + .andReturn(); } } } @@ -1181,7 +1202,7 @@ public void testTokenValiditySurpassesMaxJwtValidity() throws Exception { post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC") + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(status().is(401)); } @@ -1204,7 +1225,8 @@ public void testDebugController() throws Exception { MockHttpServletResponse response = mockMvc .perform( - get("/v1/debug/exposed/" + midnight.getTimestamp()).header("User-Agent", "MockMVC")) + get("/v1/debug/exposed/" + midnight.getTimestamp()) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()) .andReturn() .getResponse(); @@ -1272,7 +1294,7 @@ public void testNonEmptyResponseAnd304() throws Exception { mockMvc .perform( get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) - .header("User-Agent", "MockMVC")) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().isOk()) .andReturn() .getResponse(); @@ -1306,7 +1328,7 @@ public void testEtag() throws Exception { mockMvc .perform( get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) - .header("User-Agent", "MockMVC")) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()) .andReturn() .getResponse(); @@ -1319,7 +1341,7 @@ public void testEtag() throws Exception { mockMvc .perform( get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) - .header("User-Agent", "MockMVC")) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()) .andReturn() .getResponse(); @@ -1334,7 +1356,7 @@ public void testEtag() throws Exception { mockMvc .perform( get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) - .header("User-Agent", "MockMVC")) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()) .andReturn() .getResponse(); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTestNotThreadSafe.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTestNotThreadSafe.java new file mode 100644 index 00000000..4539495a --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTestNotThreadSafe.java @@ -0,0 +1,209 @@ +package org.dpppt.backend.sdk.ws.controller; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.ByteArrayInputStream; +import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.model.gaen.proto.TemporaryExposureKeyFormat; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature; +import org.junit.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@ActiveProfiles({"actuator-security"}) +@SpringBootTest( + properties = { + "ws.app.jwt.publickey=classpath://generated_pub.pem", + "logging.level.org.springframework.security=DEBUG", + "ws.exposedlist.releaseBucketDuration=7200000", + "ws.gaen.randomkeysenabled=true", + "ws.app.gaen.delayTodaysKeys=true", + "ws.monitor.prometheus.user=prometheus", + "ws.monitor.prometheus.password=prometheus", + "management.endpoints.enabled-by-default=true", + "management.endpoints.web.exposure.include=*" + }) +@Transactional +@Execution(ExecutionMode.SAME_THREAD) +public class GaenControllerTestNotThreadSafe extends BaseControllerTest { + @Autowired ProtoSignature signer; + + private static final Logger logger = LoggerFactory.getLogger(GaenControllerTest.class); + + @Test + @Transactional + public void zipContainsFiles() throws Exception { + var clockStartingAtMidnight = + Clock.offset(Clock.systemUTC(), UTCInstant.now().getDuration(UTCInstant.today()).negated()); + UTCInstant.setClock(clockStartingAtMidnight); + var now = UTCInstant.now(); + var midnight = now.atStartOfDay(); + + // insert two times 5 keys per day for the last 14 days. the second batch has a + // different received at timestamp. (+6 hours) + insertNKeysPerDayInInterval(14, midnight.minusDays(4), now, now.minusDays(1)); + + insertNKeysPerDayInInterval(14, midnight.minusDays(4), now, now.minusDays(12)); + + // request the keys with date date 1 day ago. no publish until. + MockHttpServletResponse response = + mockMvc + .perform( + get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse(); + + Long publishedUntil = Long.parseLong(response.getHeader("X-PUBLISHED-UNTIL")); + assertTrue(publishedUntil < now.getTimestamp()); + + verifyZipResponse(response, 20); + + // request again the keys with date date 1 day ago. with publish until, so that + // we only get the second batch. + var bucketAfterSecondRelease = + Duration.ofMillis(midnight.getTimestamp()) + .minusDays(1) + .plusHours(12) + .dividedBy(Duration.ofHours(2)) + * 2 + * 60 + * 60 + * 1000; + MockHttpServletResponse responseWithPublishedAfter = + mockMvc + .perform( + get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) + .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") + .param("publishedafter", Long.toString(bucketAfterSecondRelease))) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse(); + + // we always have 10 + verifyZipResponse(responseWithPublishedAfter, 10); + UTCInstant.resetClock(); + } + + private void verifyZipResponse(MockHttpServletResponse response, int expectKeyCount) + throws Exception { + ByteArrayInputStream baisOuter = new ByteArrayInputStream(response.getContentAsByteArray()); + ZipInputStream zipOuter = new ZipInputStream(baisOuter); + ZipEntry entry = zipOuter.getNextEntry(); + boolean foundData = false; + boolean foundSignature = false; + + byte[] signatureProto = null; + byte[] exportBin = null; + byte[] keyProto = null; + + while (entry != null) { + if (entry.getName().equals("export.bin")) { + foundData = true; + exportBin = zipOuter.readAllBytes(); + keyProto = new byte[exportBin.length - 16]; + System.arraycopy(exportBin, 16, keyProto, 0, keyProto.length); + } + if (entry.getName().equals("export.sig")) { + foundSignature = true; + signatureProto = zipOuter.readAllBytes(); + } + entry = zipOuter.getNextEntry(); + } + + assertTrue(foundData); + assertTrue(foundSignature); + + var list = TemporaryExposureKeyFormat.TEKSignatureList.parseFrom(signatureProto); + var export = TemporaryExposureKeyFormat.TemporaryExposureKeyExport.parseFrom(keyProto); + for (var key : export.getKeysList()) { + assertNotEquals(0, key.getRollingPeriod()); + } + var sig = list.getSignatures(0); + java.security.Signature signatureVerifier = + java.security.Signature.getInstance(sig.getSignatureInfo().getSignatureAlgorithm().trim()); + signatureVerifier.initVerify(signer.getPublicKey()); + + signatureVerifier.update(exportBin); + assertTrue(signatureVerifier.verify(sig.getSignature().toByteArray())); + assertEquals(expectKeyCount, export.getKeysCount()); + } + + private void insertNKeysPerDayInIntervalWithDebugFlag( + int n, UTCInstant start, UTCInstant end, UTCInstant receivedAt, boolean debug) + throws Exception { + var current = start; + Map rollingToCount = new HashMap<>(); + while (current.isBeforeEpochMillisOf(end)) { + List keys = new ArrayList<>(); + SecureRandom random = new SecureRandom(); + int lastRolling = (int) start.get10MinutesSince1970(); + for (int i = 0; i < n; i++) { + GaenKey key = new GaenKey(); + byte[] keyBytes = new byte[16]; + random.nextBytes(keyBytes); + key.setKeyData(Base64.getEncoder().encodeToString(keyBytes)); + key.setRollingPeriod(144); + logger.info("Rolling Start number: " + lastRolling); + key.setRollingStartNumber(lastRolling); + key.setTransmissionRiskLevel(1); + key.setFake(0); + keys.add(key); + + Integer count = rollingToCount.get(lastRolling); + if (count == null) { + count = 0; + } + count = count + 1; + rollingToCount.put(lastRolling, count); + + lastRolling -= Duration.ofDays(1).dividedBy(Duration.ofMinutes(10)); + } + if (debug) { + testGaenDataService.upsertExposeesDebug(keys, receivedAt); + } else { + testGaenDataService.upsertExposees(keys, receivedAt); + } + current = current.plusDays(1); + } + for (Entry entry : rollingToCount.entrySet()) { + logger.info( + "Rolling start number: " + + entry.getKey() + + " -> count: " + + entry.getValue() + + " (received at: " + + receivedAt.toString() + + ")"); + } + } + + private void insertNKeysPerDayInInterval( + int n, UTCInstant start, UTCInstant end, UTCInstant receivedAt) throws Exception { + insertNKeysPerDayInIntervalWithDebugFlag(n, start, end, receivedAt, false); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManagerTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManagerTest.java new file mode 100644 index 00000000..9c837414 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManagerTest.java @@ -0,0 +1,133 @@ +package org.dpppt.backend.sdk.ws.insertmanager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.semver.Version; +import org.dpppt.backend.sdk.utils.UTCInstant; +import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.OldAndroid0RPFilter; +import org.dpppt.backend.sdk.ws.util.ValidationUtils; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +public class InsertManagerTest { + @Test + public void testOSEnumWorks() { + assertEquals("Android", OSType.ANDROID.toString()); + assertEquals("iOS", OSType.IOS.toString()); + } + + @Test + public void emptyListShouldNotFail() { + Object theException = null; + try { + InsertManager manager = + new InsertManager( + new MockDataSource(), + new ValidationUtils(16, Duration.ofDays(14), Duration.ofHours(2).toMillis())); + manager.insertIntoDatabase(new ArrayList<>(), null, null, null); + } catch (Exception ex) { + theException = ex; + } + assertNull(theException); + } + + @Test + public void nullListShouldNotFail() throws Exception { + Object theException = null; + try { + InsertManager manager = + new InsertManager( + new MockDataSource(), + new ValidationUtils(16, Duration.ofDays(14), Duration.ofHours(2).toMillis())); + manager.insertIntoDatabase(null, null, null, null); + } catch (Exception ex) { + theException = ex; + } + assertNull(theException); + } + + @Test + public void wrongHeaderShouldNotFail() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger(InsertManager.class); + var appender = new TestAppender(); + appender.setContext(logger.getLoggerContext()); + appender.start(); + logger.addAppender(appender); + InsertManager manager = + new InsertManager( + new MockDataSource(), + new ValidationUtils(16, Duration.ofDays(14), Duration.ofHours(2).toMillis())); + var key = + new GaenKey("POSTMAN+POSTMAN+", (int) UTCInstant.now().get10MinutesSince1970(), 144, 0); + try { + manager.insertIntoDatabase(List.of(key), "test", null, UTCInstant.now()); + } catch (RuntimeException ex) { + if (!ex.getMessage().equals("UPSERT_EXPOSEES")) { + throw ex; + } + } + appender.stop(); + for (var event : appender.getLog()) { + assertEquals(Level.ERROR, event.getLevel()); + assertEquals("We received an invalid header, setting default.", event.getMessage()); + } + } + + @Test + public void iosRP0ShouldLog() throws Exception { + Logger logger = (Logger) LoggerFactory.getLogger(OldAndroid0RPFilter.class); + var appender = new TestAppender(); + appender.setContext(logger.getLoggerContext()); + appender.start(); + logger.addAppender(appender); + InsertManager manager = + new InsertManager( + new MockDataSource(), + new ValidationUtils(16, Duration.ofDays(14), Duration.ofHours(2).toMillis())); + manager.addFilter(new OldAndroid0RPFilter()); + var key = new GaenKey("POSTMAN+POSTMAN+", (int) UTCInstant.now().get10MinutesSince1970(), 0, 0); + try { + manager.insertIntoDatabase( + List.of(key), "org.dpppt.testrunner;1.0.0;1;iOS;29", null, UTCInstant.now()); + } catch (RuntimeException ex) { + if (!ex.getMessage().equals("UPSERT_EXPOSEES")) { + throw ex; + } + } + appender.stop(); + assertEquals(1, appender.getLog().size()); + for (var event : appender.getLog()) { + assertEquals(Level.ERROR, event.getLevel()); + assertEquals("We got a rollingPeriod of 0 ({},{},{})", event.getMessage()); + // osType, osVersion, appVersion + var osType = (OSType) event.getArgumentArray()[0]; + var osVersion = (Version) event.getArgumentArray()[1]; + var appVersion = (Version) event.getArgumentArray()[2]; + assertEquals(OSType.IOS, osType); + assertEquals("29.0.0", osVersion.toString()); + assertEquals("1.0.0+1", appVersion.toString()); + } + } + + class TestAppender extends AppenderBase { + private final List log = new ArrayList(); + + @Override + protected void append(ILoggingEvent eventObject) { + log.add(eventObject); + } + + public List getLog() { + return log; + } + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java new file mode 100644 index 00000000..c9d8fb00 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java @@ -0,0 +1,46 @@ +package org.dpppt.backend.sdk.ws.insertmanager; + +import java.time.Duration; +import java.util.List; +import org.dpppt.backend.sdk.data.gaen.GAENDataService; +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.utils.UTCInstant; + +public class MockDataSource implements GAENDataService { + + @Override + public void upsertExposees(List keys, UTCInstant now) { + throw new RuntimeException("UPSERT_EXPOSEES"); + } + + @Override + public void cleanDB(Duration retentionPeriod) {} + + @Override + public int getMaxExposedIdForKeyDate( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public List getSortedExposedForKeyDate( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getMaxExposedIdForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public List getSortedExposedForKeyDateDEBUG( + UTCInstant keyDate, UTCInstant publishedAfter, UTCInstant publishedUntil, UTCInstant now) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/util/SemverTests.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/util/SemverTests.java new file mode 100644 index 00000000..7ac64f75 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/util/SemverTests.java @@ -0,0 +1,133 @@ +package org.dpppt.backend.sdk.ws.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import org.dpppt.backend.sdk.semver.Version; +import org.junit.Test; + +public class SemverTests { + @Test + public void testToString() throws Exception { + var v = new Version("ios-1.1.3-test+meta"); + assertEquals("1.1.3-test+meta", v.toString()); + v = new Version("1.1.3+meta"); + assertEquals("1.1.3+meta", v.toString()); + v = new Version("ios-1.1.3-meta"); + assertEquals("1.1.3-meta", v.toString()); + v = new Version("ios-1.1.3"); + assertEquals("1.1.3", v.toString()); + v = new Version("1.1.3"); + assertEquals("1.1.3", v.toString()); + } + + @Test + public void testVersionFromString() throws Exception { + var cases = + List.of( + new Version("ios-0.1.0"), + new Version("android-0.1.1"), + new Version("0.2.0"), + new Version("1.0.0-prerelease"), + new Version("1.0.0"), + new Version("1.0.1+ios")); + for (int i = 0; i < cases.size(); i++) { + var currentVersion = cases.get(i); + assertTrue(currentVersion.isSameVersionAs(currentVersion)); + for (int j = 0; j < i; j++) { + var olderVersion = cases.get(j); + assertTrue(currentVersion.isLargerVersionThan(olderVersion)); + } + } + var releaseVersion = new Version("1.0.0"); + var metaInfoVersion = new Version("1.0.0+ios"); + assertTrue(releaseVersion.isSameVersionAs(metaInfoVersion)); + assertNotEquals(metaInfoVersion, releaseVersion); + var sameIosVersion = new Version("1.0.0+ios"); + assertEquals(sameIosVersion, metaInfoVersion); + } + + @Test + public void testPlatform() throws Exception { + var iosNonStandard = new Version("ios-1.0.0"); + var iosStandard = new Version("1.0.0+ios"); + assertTrue(iosNonStandard.isIOS()); + assertTrue(iosStandard.isIOS()); + assertFalse(iosNonStandard.isAndroid()); + assertFalse(iosStandard.isAndroid()); + + var androidNonStandard = new Version("android-1.0.0"); + var androidStandard = new Version("1.0.0+android"); + assertFalse(androidNonStandard.isIOS()); + assertFalse(androidStandard.isIOS()); + assertTrue(androidNonStandard.isAndroid()); + assertTrue(androidStandard.isAndroid()); + + var random = new Version("1.0.0"); + assertFalse(random.isAndroid()); + assertFalse(random.isIOS()); + } + + @Test + public void testVersionFromExplicit() throws Exception { + var cases = + List.of( + new Version(0, 1, 0), + new Version(0, 1, 1), + new Version(0, 2, 0), + new Version(1, 0, 0, "prerelease", ""), + new Version(1, 0, 0), + new Version(1, 0, 1, "", "ios")); + for (int i = 0; i < cases.size(); i++) { + var currentVersion = cases.get(i); + assertTrue(currentVersion.isSameVersionAs(currentVersion)); + for (int j = 0; j < i; j++) { + var olderVersion = cases.get(j); + assertTrue(currentVersion.isLargerVersionThan(olderVersion)); + } + } + var releaseVersion = new Version(1, 0, 0); + var metaInfoVersion = new Version(1, 0, 0, "", "ios"); + assertTrue(releaseVersion.isSameVersionAs(metaInfoVersion)); + assertNotEquals(metaInfoVersion, releaseVersion); + var sameIosVersion = new Version(1, 0, 0, "", "ios"); + assertEquals(sameIosVersion, metaInfoVersion); + } + + @Test + public void testMissingMinorOrPatch() throws Exception { + var apiLevel = "29"; + var iosVersion = "13.6"; + var apiLevelWithMeta = "29+test"; + var iosVersionWithMeta = "13.6+test"; + var apiLevelVersion = new Version(apiLevel); + assertTrue( + apiLevelVersion.getMajor() == 29 + && apiLevelVersion.getMinor() == 0 + && apiLevelVersion.getPatch() == 0); + + var iosVersionVersion = new Version(iosVersion); + assertTrue( + iosVersionVersion.getMajor() == 13 + && iosVersionVersion.getMinor() == 6 + && iosVersionVersion.getPatch() == 0); + + var apiLevelWithMetaVersion = new Version(apiLevelWithMeta); + assertTrue( + apiLevelWithMetaVersion.getMajor() == 29 + && apiLevelWithMetaVersion.getMinor() == 0 + && apiLevelWithMetaVersion.getPatch() == 0 + && apiLevelWithMetaVersion.getMetaInfo() == "test"); + + var iosVersionVersionMeta = new Version(iosVersionWithMeta); + + assertTrue( + iosVersionVersionMeta.getMajor() == 13 + && iosVersionVersionMeta.getMinor() == 6 + && iosVersionVersionMeta.getPatch() == 0 + && iosVersionVersionMeta.getMetaInfo() == "test"); + } +}