From b0ec0c6c23c6e80e2619168211c5531f3a06f093 Mon Sep 17 00:00:00 2001 From: Linus Gasser Date: Fri, 28 Aug 2020 08:16:21 +0200 Subject: [PATCH] [WIP] Feature/get exposed sql filters With the new pipeline we try to sanitise input on insert. Since due to various possible issues (and for easier debugging) we could insert keys with validity exceeding the current bucket (e.g. a key submitted today with todays keydate and rolling period 144), we additionally filter for such keys on release. Adding the rules for filtering into the SQL query should decrease the risk of accidentally releasing keys too early (e.g. bugs during insert, manipulating JWTs or other yet unknown problems). Further, this should also allow for easier testing and debugging with a query ignoring all filters. To minmise merge conflicts, this PR depends on #213 Linted and rebased - original is at #f881f566b53a335f56661041b22c3872e145a8bd --- .../backend/sdk/data/gaen/FakeKeyService.java | 7 +- .../sdk/data/gaen/GAENDataService.java | 30 ++- .../data/gaen/JDBCGAENDataServiceImpl.java | 62 +++-- .../sdk/data/DPPPTDataServiceTest.java | 2 +- .../sdk/data/gaen/GaenDataServiceTest.java | 9 +- .../gaen/PostgresGaenDataServiceTest.java | 36 +-- .../org/dpppt/backend/sdk/semver/Version.java | 249 ++++++++++++++++++ .../dpppt/backend/sdk/utils/UTCInstant.java | 18 +- .../backend/sdk/ws/config/WSBaseConfig.java | 51 ++++ .../sdk/ws/controller/DPPPTController.java | 12 +- .../sdk/ws/controller/DebugController.java | 11 +- .../sdk/ws/controller/GaenController.java | 192 ++++++-------- .../sdk/ws/insertmanager/InsertException.java | 7 + .../sdk/ws/insertmanager/InsertManager.java | 76 ++++++ .../backend/sdk/ws/insertmanager/OSType.java | 18 ++ .../insertionfilters/FakeKeysFilter.java | 21 ++ .../IOSLegacyProblemRPLT144.java | 30 +++ .../insertionfilters/InsertionFilter.java | 19 ++ .../KeysNotMatchingJWTFilter.java | 67 +++++ .../NegativeRollingPeriodFilter.java | 22 ++ .../insertionfilters/NoBase64Filter.java | 44 ++++ .../insertionfilters/OldAndroid0RPFilter.java | 36 +++ ...llingStartNumberAfterDayAfterTomorrow.java | 30 +++ .../RollingStartNumberBeforeRetentionDay.java | 35 +++ .../sdk/ws/security/JWTValidateRequest.java | 9 +- .../sdk/ws/security/NoValidateRequest.java | 4 +- .../sdk/ws/security/ValidateRequest.java | 18 +- .../ws/security/gaen/JWTValidateRequest.java | 25 +- .../backend/sdk/ws/util/ValidationUtils.java | 52 ++++ .../sdk/ws/controller/BaseControllerTest.java | 4 +- .../sdk/ws/controller/GaenControllerTest.java | 216 ++++++++------- .../GaenControllerTestNotThreadSafe.java | 209 +++++++++++++++ .../ws/insertmanager/InsertManagerTest.java | 133 ++++++++++ .../sdk/ws/insertmanager/MockDataSource.java | 46 ++++ .../backend/sdk/ws/util/SemverTests.java | 133 ++++++++++ 35 files changed, 1622 insertions(+), 311 deletions(-) create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/semver/Version.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertException.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManager.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/OSType.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/FakeKeysFilter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/IOSLegacyProblemRPLT144.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/InsertionFilter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/KeysNotMatchingJWTFilter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NegativeRollingPeriodFilter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/NoBase64Filter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/OldAndroid0RPFilter.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberAfterDayAfterTomorrow.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/insertmanager/insertionfilters/RollingStartNumberBeforeRetentionDay.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTestNotThreadSafe.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/InsertManagerTest.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java create mode 100644 dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/util/SemverTests.java 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"); + } +}