You will be provided with a partial code base and an issue statement explaining a problem to resolve.
<issue>
Ensure uniqueness of diagnosis keys
## Current Implementation
The uniqueness of diagnosis keys is neither guaranteed by the database, nor by validation logic.

## Suggested Enhancement
Ensure that diagnosis keys in the database are unique (there are no duplicates).

What needs to be decided:
- [x] Is it sufficient to guarantee the uniqueness of the diagnosis key or should the uniqueness of diagnosis key + submission timestamp (+maybe even more fields) be guaranteed?
- [x] Should the duplicate check happen on the database (`UNIQUE` constraint) or in the server logic (e.g. during the validation of submitted keys)?
- [x] If the check happens on the database: Should the `id` column be removed and replaced with the diagnosis key (+submission timestamp) as (composite) primary key?
- [x] How should the server react when a duplicate diagnosis key is submitted (update, error, etc.)?

**Edit:** Decisions are below.

</issue>

<code>
[start of README.md]
1 <!-- markdownlint-disable MD041 -->
2 <h1 align="center">
3     Corona-Warn-App Server
4 </h1>
5 
6 <p align="center">
7     <a href="https://github.com/corona-warn-app/cwa-server/commits/" title="Last Commit"><img src="https://img.shields.io/github/last-commit/corona-warn-app/cwa-server?style=flat"></a>
8     <a href="https://github.com/corona-warn-app/cwa-server/issues" title="Open Issues"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-server?style=flat"></a>
9     <a href="https://circleci.com/gh/corona-warn-app/cwa-server" title="Build Status"><img src="https://circleci.com/gh/corona-warn-app/cwa-server.svg?style=shield&circle-token=4ab059989d10709df19eb4b98ab7c121a25e981a"></a>
10         <a href="https://sonarcloud.io/dashboard?id=corona-warn-app_cwa-server" title="Quality Gate"><img src="https://sonarcloud.io/api/project_badges/measure?project=corona-warn-app_cwa-server&metric=alert_status"></a>
11         <a href="https://sonarcloud.io/component_measures?id=corona-warn-app_cwa-server&metric=Coverage&view=list" title="Coverage"><img src="https://sonarcloud.io/api/project_badges/measure?project=corona-warn-app_cwa-server&metric=coverage"></a>
12     <a href="https://github.com/corona-warn-app/cwa-server/blob/master/LICENSE" title="License"><img src="https://img.shields.io/badge/License-Apache%202.0-green.svg?style=flat"></a>
13 </p>
14 
15 <p align="center">
16   <a href="#development">Development</a> •
17   <a href="#service-apis">Service APIs</a> •
18   <a href="#documentation">Documentation</a> •
19   <a href="#support-and-feedback">Support</a> •
20   <a href="#how-to-contribute">Contribute</a> •
21   <a href="#contributors">Contributors</a> •
22   <a href="#repositories">Repositories</a> •
23   <a href="#licensing">Licensing</a>
24 </p>
25 
26 The goal of this project is to develop the official Corona-Warn-App for Germany based on the exposure notification API from [Apple](https://www.apple.com/covid19/contacttracing/) and [Google](https://www.google.com/covid19/exposurenotifications/). The apps (for both iOS and Android) use Bluetooth technology to exchange anonymous encrypted data with other mobile phones (on which the app is also installed) in the vicinity of an app user's phone. The data is stored locally on each user's device, preventing authorities or other parties from accessing or controlling the data. This repository contains the **implementation of the server for encryption keys** for the Corona-Warn-App. This implementation is still a **work in progress**, and the code it contains is currently alpha-quality code.
27 
28 In this documentation, Corona-Warn-App services are also referred to as CWA services.
29 
30 ## Architecture Overview
31 
32 You can find the architecture overview [here](/docs/architecture-overview.md), which will give you
33 a good starting point in how the backend services interact with other services, and what purpose
34 they serve.
35 
36 ## Development
37 
38 After you've checked out this repository, you can run the application in one of the following ways:
39 
40 * As a [Docker](https://www.docker.com/)-based deployment on your local machine. You can run either:
41   * Single components using the respective Dockerfile or
42   * The full backend using the Docker Compose (which is considered the most convenient way)
43 * As a [Maven](https://maven.apache.org)-based build on your local machine.
44   If you want to develop something in a single component, this approach is preferable.
45 
46 ### Docker-Based Deployment
47 
48 If you want to use Docker-based deployment, you need to install Docker on your local machine. For more information about downloading and installing Docker, see the [official Docker documentation](https://docs.docker.com/get-docker/).
49 
50 #### Running the Full CWA Backend Using Docker Compose
51 
52 For your convenience, a full setup including the generation of test data has been prepared using [Docker Compose](https://docs.docker.com/compose/reference/overview/). To build the backend services, run ```docker-compose build``` in the repository's root directory. A default configuration file can be found under ```.env```in the root folder of the repository. The default values for the local Postgres and Zenko Cloudserver should be changed in this file before docker-compose is run.
53 
54 Once the services are built, you can start the whole backend using ```docker-compose up```.
55 The distribution service runs once and then finishes. If you want to trigger additional distribution runs, run ```docker-compose run distribution```.
56 
57 The docker-compose contains the following services:
58 
59 Service           | Description | Endpoint and Default Credentials
60 ------------------|-------------|-----------
61 submission        | The Corona-Warn-App submission service                                                      | `http://localhost:8000` <br> `http://localhost:8006` (for actuator endpoint)
62 distribution      | The Corona-Warn-App distribution service                                                    | NO ENDPOINT
63 postgres          | A [postgres] database installation                                                          | `postgres:8001` <br> Username: postgres <br> Password: postgres
64 pgadmin           | A [pgadmin](https://www.pgadmin.org/) installation for the postgres database                | `http://localhost:8002` <br> Username: user@domain.com <br> Password: password
65 cloudserver       | [Zenko CloudServer] is a S3-compliant object store  | `http://localhost:8003/` <br> Access key: accessKey1 <br> Secret key: verySecretKey1
66 verification-fake | A very simple fake implementation for the tan verification.                                 | `http://localhost:8004/version/v1/tan/verify` <br> The only valid tan is "edc07f08-a1aa-11ea-bb37-0242ac130002"
67 
68 ##### Known Limitation
69 
70 In rare cases the docker-compose runs into a timing issue if the distribution service starts before the bucket of the objectstore was created. This is not a big issue as you can simply run ```docker-compose run distribution``` to trigger additional distribution runs after the objectstore was initialized.
71 
72 #### Running Single CWA Services Using Docker
73 
74 If you would like to build and run a single CWA service, it's considered easiest to run them in a Docker environment. You can do this using the script provided in the respective CWA service directory. The Docker script first builds the CWA service and then creates an image for the runtime, which means that there are no additional dependencies for you to install.
75 
76 To build and run the distribution service, run the following command:
77 
78 ```bash
79 ./services/distribution/build_and_run.sh
80 ```
81 
82 To build and run the submission service, run the following command:
83 
84 ```bash
85 ./services/submission/build_and_run.sh
86 ```
87 
88 The submission service is available on [localhost:8080](http://localhost:8080 ).
89 
90 ### Maven-Based Build
91 
92 If you want to actively develop in one of the CWA services, the Maven-based runtime is most suitable.
93 To prepare your machine to run the CWA project locally, we recommend that you first ensure that you've installed the following:
94 
95 * Minimum JDK Version 11: [OpenJDK](https://openjdk.java.net/) / [SapMachine](https://sap.github.io/SapMachine/)
96 * [Maven 3.6](https://maven.apache.org/)
97 * [Postgres]
98 * [Zenko CloudServer]
99 
100 #### Configure
101 
102 After you made sure that the specified dependencies are running, configure them in the respective configuration files.
103 
104 * Configure the Postgres connection in the [submission config](./services/submission/src/main/resources/application.yaml) and in the [distribution config](./services/distribution/src/main/resources/application.yaml)
105 * Configure the S3 compatible object storage in the [distribution config](./services/distribution/src/main/resources/application.yaml)
106 * Configure the private key for the distribution service, the path need to be prefixed with `file:`
107   * `VAULT_FILESIGNING_SECRET` should be the path to the private key, example available in `<repo-root>/docker-compose-test-secrets/private.pem`
108 
109 #### Build
110 
111 After you've checked out the repository, to build the project, run ```mvn install``` in your base directory.
112 
113 #### Run
114 
115 Navigate to the service you want to start and run the spring-boot:run target. The configured Postgres and the configured S3 compliant object storage are used as default. When you start the submission service, the endpoint is available on your local port 8080.
116 
117 If you want to start the submission service, for example, you start it as follows:
118 
119 ```bash
120   cd services/submission/
121   mvn spring-boot:run
122 ```
123 
124 #### Debugging
125 
126 To enable the `DEBUG` log level, you can run the application using the Spring `dev` profile.
127 
128 ```bash
129 mvn spring-boot:run -Dspring-boot.run.profiles=dev
130 ```
131 
132 To be able to set breakpoints (e.g. in IntelliJ), it may be necessary to use the ```-Dspring-boot.run.fork=false``` parameter.
133 
134 ## Service APIs
135 
136 The API that is being exposed by the backend services is documented in an [OpenAPI](https://www.openapis.org/) specification. The specification files are available at the following locations:
137 
138 Service      | OpenAPI Specification
139 -------------|-------------
140 Submission Service        | [services/submission/api_v1.json](https://github.com/corona-warn-app/cwa-server/raw/master/services/submission/api_v1.json)
141 Distribution Service      | [services/distribution/api_v1.json](https://github.com/corona-warn-app/cwa-server/raw/master/services/distribution/api_v1.json)
142 
143 ## Spring Profiles
144 
145 ### Distribution
146 
147 Profile          | Effect
148 -----------------|-------------
149 `dev`            | Turns the log level to `DEBUG`.
150 `cloud`          | Removes default values for the `datasource` and `objectstore` configurations.
151 `demo`           | Includes incomplete days and hours into the distribution run, thus creating aggregates for the current day and the current hour (and including both in the respective indices). When running multiple distributions in one hour with this profile, the date aggregate for today and the hours aggregate for the current hour will be updated and overwritten.
152 `testdata`       | Causes test data to be inserted into the database before each distribution run. By default, around 1000 random diagnosis keys will be generated per hour. If there are no diagnosis keys in the database yet, random keys will be generated for every hour from the beginning of the retention period (14 days ago at 00:00 UTC) until one hour before the present hour. If there are already keys in the database, the random keys will be generated for every hour from the latest diagnosis key in the database (by submission timestamp) until one hour before the present hour (or none at all, if the latest diagnosis key in the database was submitted one hour ago or later).
153 `signature-dev`  | Sets the app package ID in the export packages' signature info to `de.rki.coronawarnapp-dev` so that test certificates (instead of production certificates) will be used for client-side validation.
154 `signature-prod` | Provides production app package IDs for the signature info
155 
156 ### Submission
157 
158 Profile      | Effect
159 -------------|-------------
160 `dev`        | Turns the log level to `DEBUG`.
161 `cloud`      | Removes default values for the `datasource` configuration.
162 
163 ## Documentation
164 
165 The full documentation for the Corona-Warn-App can be found in the [cwa-documentation](https://github.com/corona-warn-app/cwa-documentation) repository. The documentation repository contains technical documents, architecture information, and whitepapers related to this implementation.
166 
167 ## Support and Feedback
168 
169 The following channels are available for discussions, feedback, and support requests:
170 
171 | Type                     | Channel                                                |
172 | ------------------------ | ------------------------------------------------------ |
173 | **General Discussion**   | <a href="https://github.com/corona-warn-app/cwa-documentation/issues/new/choose" title="General Discussion"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-documentation/question.svg?style=flat-square"></a> </a>   |
174 | **Concept Feedback**    | <a href="https://github.com/corona-warn-app/cwa-documentation/issues/new/choose" title="Open Concept Feedback"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-documentation/architecture.svg?style=flat-square"></a>  |
175 | **Backend Issue**    | <a href="https://github.com/corona-warn-app/cwa-server/issues/new/choose" title="Open Backend Issue"><img src="https://img.shields.io/github/issues/corona-warn-app/cwa-server?style=flat-square"></a>  |
176 | **Other Requests**    | <a href="mailto:corona-warn-app.opensource@sap.com" title="Email CWA Team"><img src="https://img.shields.io/badge/email-CWA%20team-green?logo=mail.ru&style=flat-square&logoColor=white"></a>   |
177 
178 ## How to Contribute
179 
180 Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](./CONTRIBUTING.md). By participating in this project, you agree to abide by its [Code of Conduct](./CODE_OF_CONDUCT.md) at all times.
181 
182 ## Contributors
183 
184 The German government has asked SAP and Deutsche Telekom to develop the Corona-Warn-App for Germany as open source software. Deutsche Telekom is providing the network and mobile technology and will operate and run the backend for the app in a safe, scalable and stable manner. SAP is responsible for the app development, its framework and the underlying platform. Therefore, development teams of SAP and Deutsche Telekom are contributing to this project. At the same time our commitment to open source means that we are enabling -in fact encouraging- all interested parties to contribute and become part of its developer community.
185 
186 ## Repositories
187 
188 The following public repositories are currently available for the Corona-Warn-App:
189 
190 | Repository          | Description                                                           |
191 | ------------------- | --------------------------------------------------------------------- |
192 | [cwa-documentation] | Project overview, general documentation, and white papers            |
193 | [cwa-server]        | Backend implementation for the Apple/Google exposure notification API|
194 | [cwa-verification-server] | Backend implementation of the verification process|
195 
196 [cwa-documentation]: https://github.com/corona-warn-app/cwa-documentation
197 [cwa-server]: https://github.com/corona-warn-app/cwa-server
198 [cwa-verification-server]: https://github.com/corona-warn-app/cwa-verification-server
199 [Postgres]: https://www.postgresql.org/
200 [HSQLDB]: http://hsqldb.org/
201 [Zenko CloudServer]: https://github.com/scality/cloudserver
202 
203 ## Licensing
204 
205 Copyright (c) 2020 SAP SE or an SAP affiliate company.
206 
207 Licensed under the **Apache License, Version 2.0** (the "License"); you may not use this file except in compliance with the License.
208 
209 You may obtain a copy of the License at <https://www.apache.org/licenses/LICENSE-2.0>.
210 
211 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the [LICENSE](./LICENSE) for the specific language governing permissions and limitations under the License.
212 
[end of README.md]
[start of /dev/null]
1 
[end of /dev/null]
[start of common/persistence/pom.xml]
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5   <parent>
6     <groupId>org.springframework.boot</groupId>
7     <artifactId>spring-boot-starter-parent</artifactId>
8     <version>2.3.0.RELEASE</version>
9     <relativePath></relativePath>
10   </parent>
11   <modelVersion>4.0.0</modelVersion>
12 
13   <groupId>org.opencwa</groupId>
14   <artifactId>persistence</artifactId>
15   <version>${revision}</version>
16 
17   <organization>
18     <name>SAP SE</name>
19   </organization>
20 
21   <properties>
22     <java.version>11</java.version>
23     <maven.compiler.source>11</maven.compiler.source>
24     <maven.compiler.target>11</maven.compiler.target>
25     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
26     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
27     <sonar.projectKey>corona-warn-app_cwa-server_common_persistence</sonar.projectKey>
28   </properties>
29 
30   <dependencies>
31     <dependency>
32       <groupId>org.opencwa</groupId>
33       <artifactId>protocols</artifactId>
34       <version>${project.version}</version>
35     </dependency>
36     <dependency>
37       <groupId>org.springframework.boot</groupId>
38       <artifactId>spring-boot-starter-data-jpa</artifactId>
39     </dependency>
40     <dependency>
41       <groupId>org.springframework.boot</groupId>
42       <artifactId>spring-boot-starter-validation</artifactId>
43     </dependency>
44     <dependency>
45       <groupId>org.springframework.boot</groupId>
46       <artifactId>spring-boot-starter-test</artifactId>
47       <scope>test</scope>
48       <exclusions>
49         <exclusion>
50           <groupId>org.junit.vintage</groupId>
51           <artifactId>junit-vintage-engine</artifactId>
52         </exclusion>
53       </exclusions>
54     </dependency>
55     <dependency>
56       <groupId>org.springframework.boot</groupId>
57       <artifactId>spring-boot-starter</artifactId>
58       <exclusions>
59         <exclusion>
60           <groupId>org.springframework.boot</groupId>
61           <artifactId>spring-boot-starter-logging</artifactId>
62         </exclusion>
63       </exclusions>
64     </dependency>
65     <dependency>
66       <groupId>org.springframework.boot</groupId>
67       <artifactId>spring-boot-starter-log4j2</artifactId>
68     </dependency>
69     <dependency>
70       <groupId>org.flywaydb</groupId>
71       <artifactId>flyway-core</artifactId>
72     </dependency>
73     <dependency>
74       <groupId>org.postgresql</groupId>
75       <artifactId>postgresql</artifactId>
76       <scope>runtime</scope>
77     </dependency>
78     <dependency>
79       <groupId>com.h2database</groupId>
80       <artifactId>h2</artifactId>
81       <version>1.4.199</version>
82       <scope>runtime</scope>
83     </dependency>
84     <dependency>
85       <!-- https://nvd.nist.gov/vuln/detail/CVE-2020-9488 -->
86       <groupId>org.apache.logging.log4j</groupId>
87       <artifactId>log4j-core</artifactId>
88       <version>2.13.3</version>
89     </dependency>
90     <dependency>
91       <!-- https://nvd.nist.gov/vuln/detail/CVE-2020-9488 -->
92       <groupId>org.apache.logging.log4j</groupId>
93       <artifactId>log4j-api</artifactId>
94       <version>2.13.3</version>
95     </dependency>
96     <dependency>
97       <!-- Due to https://nvd.nist.gov/vuln/detail/CVE-2020-10683 -->
98       <groupId>org.dom4j</groupId>
99       <artifactId>dom4j</artifactId>
100       <version>2.1.3</version>
101     </dependency>
102     <dependency>
103       <!-- https://nvd.nist.gov/vuln/detail/CVE-2017-18640 -->
104       <groupId>org.yaml</groupId>
105       <artifactId>snakeyaml</artifactId>
106       <version>1.26</version>
107     </dependency>
108   </dependencies>
109 
110   <build>
111     <plugins>
112       <plugin>
113         <groupId>org.codehaus.mojo</groupId>
114         <artifactId>license-maven-plugin</artifactId>
115         <version>2.0.0</version>
116         <configuration>
117           <includes>**/*.java</includes>
118           <copyrightOwners>${organization.name} and all other contributors</copyrightOwners>
119           <processStartTag>---license-start</processStartTag>
120           <processEndTag>---license-end</processEndTag>
121           <sectionDelimiter>---</sectionDelimiter>
122           <addJavaLicenseAfterPackage>false</addJavaLicenseAfterPackage>
123           <trimHeaderLine>true</trimHeaderLine>
124           <emptyLineAfterHeader>true</emptyLineAfterHeader>
125         </configuration>
126       </plugin>
127       <plugin>
128         <!-- see https://maven.apache.org/maven-ci-friendly.html#install-deploy -->
129         <groupId>org.codehaus.mojo</groupId>
130         <artifactId>flatten-maven-plugin</artifactId>
131         <version>1.1.0</version>
132         <configuration>
133           <updatePomFile>true</updatePomFile>
134           <flattenMode>resolveCiFriendliesOnly</flattenMode>
135         </configuration>
136         <executions>
137           <execution>
138             <id>flatten</id>
139             <phase>process-resources</phase>
140             <goals><goal>flatten</goal></goals>
141           </execution>
142           <execution>
143             <id>flatten-clean</id>
144             <phase>clean</phase>
145             <goals><goal>clean</goal></goals>
146           </execution>
147         </executions>
148       </plugin>
149       <plugin>
150         <groupId>org.apache.maven.plugins</groupId>
151         <artifactId>maven-checkstyle-plugin</artifactId>
152         <version>3.1.1</version>
153         <configuration>
154           <configLocation>../../codestyle/checkstyle.xml</configLocation>
155           <excludes>**/module-info.java,**/target/**/*</excludes>
156           <encoding>UTF-8</encoding>
157           <consoleOutput>true</consoleOutput>
158           <failsOnError>true</failsOnError>
159           <violationSeverity>warning</violationSeverity>
160           <failOnViolation>true</failOnViolation>
161           <linkXRef>false</linkXRef>
162         </configuration>
163         <executions>
164           <execution>
165             <id>validate</id>
166             <phase>validate</phase>
167             <goals>
168               <goal>check</goal>
169             </goals>
170           </execution>
171         </executions>
172       </plugin>
173       <plugin>
174         <groupId>org.jacoco</groupId>
175         <artifactId>jacoco-maven-plugin</artifactId>
176         <version>0.8.5</version>
177         <executions>
178           <execution>
179             <goals>
180               <goal>prepare-agent</goal>
181             </goals>
182           </execution>
183           <execution>
184             <id>report</id>
185             <goals>
186               <goal>report</goal>
187             </goals>
188             <phase>verify</phase>
189           </execution>
190         </executions>
191       </plugin>
192     </plugins>
193   </build>
194 </project>
195 
[end of common/persistence/pom.xml]
[start of common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java]
1 /*-
2  * ---license-start
3  * Corona-Warn-App
4  * ---
5  * Copyright (C) 2020 SAP SE and all other contributors
6  * ---
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ---license-end
19  */
20 
21 package app.coronawarn.server.common.persistence.domain;
22 
23 import static java.time.ZoneOffset.UTC;
24 
25 import app.coronawarn.server.common.persistence.domain.DiagnosisKeyBuilders.Builder;
26 import app.coronawarn.server.common.persistence.domain.validation.ValidRollingStartIntervalNumber;
27 import java.time.Instant;
28 import java.time.LocalDateTime;
29 import java.util.Arrays;
30 import java.util.Objects;
31 import java.util.Set;
32 import javax.persistence.Entity;
33 import javax.persistence.GeneratedValue;
34 import javax.persistence.GenerationType;
35 import javax.persistence.Id;
36 import javax.persistence.Table;
37 import javax.validation.ConstraintViolation;
38 import javax.validation.Validation;
39 import javax.validation.Validator;
40 import javax.validation.constraints.Size;
41 import org.hibernate.validator.constraints.Range;
42 
43 /**
44  * A key generated for advertising over a window of time.
45  */
46 @Entity
47 @Table(name = "diagnosis_key")
48 public class DiagnosisKey {
49 
50   /**
51    * According to "Setting Up an Exposure Notification Server" by Apple, exposure notification servers are expected to
52    * reject any diagnosis keys that do not have a rolling period of a certain fixed value. See
53    * https://developer.apple.com/documentation/exposurenotification/setting_up_an_exposure_notification_server
54    */
55   public static final int EXPECTED_ROLLING_PERIOD = 144;
56 
57   private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
58 
59   @Id
60   @GeneratedValue(strategy = GenerationType.IDENTITY)
61   private Long id;
62 
63   @Size(min = 16, max = 16, message = "Key data must be a byte array of length 16.")
64   private byte[] keyData;
65 
66   @ValidRollingStartIntervalNumber
67   private int rollingStartIntervalNumber;
68 
69   @Range(min = EXPECTED_ROLLING_PERIOD, max = EXPECTED_ROLLING_PERIOD,
70       message = "Rolling period must be " + EXPECTED_ROLLING_PERIOD + ".")
71   private int rollingPeriod;
72 
73   @Range(min = 0, max = 8, message = "Risk level must be between 0 and 8.")
74   private int transmissionRiskLevel;
75 
76   private long submissionTimestamp;
77 
78   protected DiagnosisKey() {
79   }
80 
81   /**
82    * Should be called by builders.
83    */
84   DiagnosisKey(byte[] keyData, int rollingStartIntervalNumber, int rollingPeriod,
85       int transmissionRiskLevel, long submissionTimestamp) {
86     this.keyData = keyData;
87     this.rollingStartIntervalNumber = rollingStartIntervalNumber;
88     this.rollingPeriod = rollingPeriod;
89     this.transmissionRiskLevel = transmissionRiskLevel;
90     this.submissionTimestamp = submissionTimestamp;
91   }
92 
93   /**
94    * Returns a DiagnosisKeyBuilder instance. A {@link DiagnosisKey} can then be build by either providing the required
95    * member values or by passing the respective protocol buffer object.
96    *
97    * @return DiagnosisKeyBuilder instance.
98    */
99   public static Builder builder() {
100     return new DiagnosisKeyBuilder();
101   }
102 
103   public Long getId() {
104     return id;
105   }
106 
107   /**
108    * Returns the diagnosis key.
109    */
110   public byte[] getKeyData() {
111     return keyData;
112   }
113 
114   /**
115    * Returns a number describing when a key starts. It is equal to startTimeOfKeySinceEpochInSecs / (60 * 10).
116    */
117   public int getRollingStartIntervalNumber() {
118     return rollingStartIntervalNumber;
119   }
120 
121   /**
122    * Returns a number describing how long a key is valid. It is expressed in increments of 10 minutes (e.g. 144 for 24
123    * hours).
124    */
125   public int getRollingPeriod() {
126     return rollingPeriod;
127   }
128 
129   /**
130    * Returns the risk of transmission associated with the person this key came from.
131    */
132   public int getTransmissionRiskLevel() {
133     return transmissionRiskLevel;
134   }
135 
136   /**
137    * Returns the timestamp associated with the submission of this {@link DiagnosisKey} as hours since epoch.
138    */
139   public long getSubmissionTimestamp() {
140     return submissionTimestamp;
141   }
142 
143   /**
144    * Checks if this diagnosis key falls into the period between now, and the retention threshold.
145    *
146    * @param daysToRetain the number of days before a key is outdated
147    * @return true, if the rolling start interval number is within the time between now, and the given days to retain
148    * @throws IllegalArgumentException if {@code daysToRetain} is negative.
149    */
150   public boolean isYoungerThanRetentionThreshold(int daysToRetain) {
151     if (daysToRetain < 0) {
152       throw new IllegalArgumentException("Retention threshold must be greater or equal to 0.");
153     }
154     long threshold = LocalDateTime
155         .ofInstant(Instant.now(), UTC)
156         .minusDays(daysToRetain)
157         .toEpochSecond(UTC) / (60 * 10);
158 
159     return this.rollingStartIntervalNumber >= threshold;
160   }
161 
162   /**
163    * Gets any constraint violations that this key might incorporate.
164    *
165    * <p><ul>
166    * <li>Risk level must be between 0 and 8
167    * <li>Rolling start interval number must be greater than 0
168    * <li>Rolling start number cannot be in the future
169    * <li>Rolling period must be positive number
170    * <li>Key data must be byte array of length 16
171    * </ul>
172    *
173    * @return A set of constraint violations of this key.
174    */
175   public Set<ConstraintViolation<DiagnosisKey>> validate() {
176     return VALIDATOR.validate(this);
177   }
178 
179   @Override
180   public boolean equals(Object o) {
181     if (this == o) {
182       return true;
183     }
184     if (o == null || getClass() != o.getClass()) {
185       return false;
186     }
187     DiagnosisKey that = (DiagnosisKey) o;
188     return rollingStartIntervalNumber == that.rollingStartIntervalNumber
189         && rollingPeriod == that.rollingPeriod
190         && transmissionRiskLevel == that.transmissionRiskLevel
191         && submissionTimestamp == that.submissionTimestamp
192         && Objects.equals(id, that.id)
193         && Arrays.equals(keyData, that.keyData);
194   }
195 
196   @Override
197   public int hashCode() {
198     int result = Objects
199         .hash(id, rollingStartIntervalNumber, rollingPeriod, transmissionRiskLevel, submissionTimestamp);
200     result = 31 * result + Arrays.hashCode(keyData);
201     return result;
202   }
203 }
204 
[end of common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java]
[start of common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java]
1 /*-
2  * ---license-start
3  * Corona-Warn-App
4  * ---
5  * Copyright (C) 2020 SAP SE and all other contributors
6  * ---
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ---license-end
19  */
20 
21 package app.coronawarn.server.common.persistence.repository;
22 
23 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
24 import org.springframework.data.jpa.repository.JpaRepository;
25 import org.springframework.stereotype.Repository;
26 
27 @Repository
28 public interface DiagnosisKeyRepository extends JpaRepository<DiagnosisKey, Long> {
29 
30   /**
31    * Deletes all entries that have a submission timestamp lesser or equal to the specified one.
32    *
33    * @param submissionTimestamp the submission timestamp up to which entries will be deleted.
34    */
35   void deleteBySubmissionTimestampIsLessThanEqual(long submissionTimestamp);
36 }
37 
[end of common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java]
[start of common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java]
1 /*-
2  * ---license-start
3  * Corona-Warn-App
4  * ---
5  * Copyright (C) 2020 SAP SE and all other contributors
6  * ---
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ---license-end
19  */
20 
21 package app.coronawarn.server.common.persistence.service;
22 
23 import static java.time.ZoneOffset.UTC;
24 
25 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
26 import app.coronawarn.server.common.persistence.repository.DiagnosisKeyRepository;
27 import java.time.Instant;
28 import java.time.LocalDateTime;
29 import java.util.Collection;
30 import java.util.List;
31 import java.util.stream.Collectors;
32 import javax.validation.ConstraintViolation;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.springframework.beans.factory.annotation.Autowired;
36 import org.springframework.data.domain.Sort;
37 import org.springframework.data.domain.Sort.Direction;
38 import org.springframework.stereotype.Component;
39 import org.springframework.transaction.annotation.Transactional;
40 
41 @Component
42 public class DiagnosisKeyService {
43 
44   private static final Logger logger = LoggerFactory.getLogger(DiagnosisKeyService.class);
45   private final DiagnosisKeyRepository keyRepository;
46 
47   @Autowired
48   public DiagnosisKeyService(DiagnosisKeyRepository keyRepository) {
49     this.keyRepository = keyRepository;
50   }
51 
52   /**
53    * Persists the specified collection of {@link DiagnosisKey} instances.
54    *
55    * @param diagnosisKeys must not contain {@literal null}.
56    * @throws IllegalArgumentException in case the given collection contains {@literal null}.
57    */
58   public void saveDiagnosisKeys(Collection<DiagnosisKey> diagnosisKeys) {
59     keyRepository.saveAll(diagnosisKeys);
60   }
61 
62   /**
63    * Returns all valid persisted diagnosis keys, sorted by their submission timestamp.
64    */
65   public List<DiagnosisKey> getDiagnosisKeys() {
66     return keyRepository.findAll(Sort.by(Direction.ASC, "submissionTimestamp")).stream()
67         .filter(DiagnosisKeyService::isDiagnosisKeyValid).collect(Collectors.toList());
68   }
69 
70   private static boolean isDiagnosisKeyValid(DiagnosisKey diagnosisKey) {
71     Collection<ConstraintViolation<DiagnosisKey>> violations = diagnosisKey.validate();
72     boolean isValid = violations.isEmpty();
73 
74     if (!isValid) {
75       List<String> violationMessages =
76           violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
77       logger.warn("Validation failed for diagnosis key from database. Violations: {}", violationMessages);
78     }
79 
80     return isValid;
81   }
82 
83   /**
84    * Deletes all diagnosis key entries which have a submission timestamp that is older than the specified number of
85    * days.
86    *
87    * @param daysToRetain the number of days until which diagnosis keys will be retained.
88    * @throws IllegalArgumentException if {@code daysToRetain} is negative.
89    */
90   @Transactional
91   public void applyRetentionPolicy(int daysToRetain) {
92     if (daysToRetain < 0) {
93       throw new IllegalArgumentException("Number of days to retain must be greater or equal to 0.");
94     }
95 
96     long threshold = LocalDateTime
97         .ofInstant(Instant.now(), UTC)
98         .minusDays(daysToRetain)
99         .toEpochSecond(UTC) / 3600L;
100     keyRepository.deleteBySubmissionTimestampIsLessThanEqual(threshold);
101   }
102 }
103 
[end of common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java]
</code>

Here is an example of a patch file. It consists of changes to the code base. It specifies the file names, the line numbers of each change, and the removed and added lines. A single patch file can contain changes to multiple files.
<patch>
--- a/file.py
+++ b/file.py
@@ -1,27 +1,35 @@
 def euclidean(a, b):
-    while b:
-        a, b = b, a % b
-    return a
+    if b == 0:
+        return a
+    return euclidean(b, a % b)
 
 
 def bresenham(x0, y0, x1, y1):
     points = []
     dx = abs(x1 - x0)
     dy = abs(y1 - y0)
-    sx = 1 if x0 < x1 else -1
-    sy = 1 if y0 < y1 else -1
-    err = dx - dy
+    x, y = x0, y0
+    sx = -1 if x0 > x1 else 1
+    sy = -1 if y0 > y1 else 1
 
-    while True:
-        points.append((x0, y0))
-        if x0 == x1 and y0 == y1:
-            break
-        e2 = 2 * err
-        if e2 > -dy:
+    if dx > dy:
+        err = dx / 2.0
+        while x != x1:
+            points.append((x, y))
             err -= dy
-            x0 += sx
-        if e2 < dx:
-            err += dx
-            y0 += sy
+            if err < 0:
+                y += sy
+                err += dx
+            x += sx
+    else:
+        err = dy / 2.0
+        while y != y1:
+            points.append((x, y))
+            err -= dx
+            if err < 0:
+                x += sx
+                err += dy
+            y += sy
 
+    points.append((x, y))
     return points
</patch>

I need you to solve the provded issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the format shown above.
Respond below:


Ensure uniqueness of diagnosis keys
## Current Implementation
The uniqueness of diagnosis keys is neither guaranteed by the database, nor by validation logic.

## Suggested Enhancement
Ensure that diagnosis keys in the database are unique (there are no duplicates).

What needs to be decided:
- [x] Is it sufficient to guarantee the uniqueness of the diagnosis key or should the uniqueness of diagnosis key + submission timestamp (+maybe even more fields) be guaranteed?
- [x] Should the duplicate check happen on the database (`UNIQUE` constraint) or in the server logic (e.g. during the validation of submitted keys)?
- [x] If the check happens on the database: Should the `id` column be removed and replaced with the diagnosis key (+submission timestamp) as (composite) primary key?
- [x] How should the server react when a duplicate diagnosis key is submitted (update, error, etc.)?

**Edit:** Decisions are below.

Decisions made by @christian-kirschnick:
- Ensuring the uniqueness of the diagnosis key is sufficient
- Duplicate check should happen on the database
- `id` column should be removed
- `key_data` column should become primary key
- If we encounter problems with the current type (`bytea`) of `key_data` as primary key, then change the type to a base64 encoded String
- When duplicate keys are submitted, the server should just ignore them (no update, no error).
@pithumke Was referenced back to here from a different issue, What would be the value of base64 encoding the UUID? it'll just take up even more space than even using char(32). there is a UUID type already in pg that'll give better seek performance and cost 16 bytes per entry instead of 32

About the risk of collision itself, with UUIDv4 that is quite unlikely even with billions of records, However if you don't want to suffer the occasional exception on the server side you can just add `ON CONFLICT DO NOTHING` to swallow the error 
Hi @oiime, thank you for the remarks! Good point with the UUID type, I'll bring it up in the team discussions and report back. However, please note that, to my knowledge, the type of the diagnosis keys is not explicitly stated as UUID by the Google/Apple spec, but only as "Data" (see [here](https://developer.apple.com/documentation/exposurenotification/entemporaryexposurekey/3583679-keydata)) containing 16 bytes (see [here](https://www.blog.google/documents/60/Exposure_Notification_-_Cryptography_Specification_v1.1.pdf)). This can, in theory, be mapped to a UUID, but I would like to investigate if the PG UUID type implies any restrictions. For example, UUIDv4 reserves 6 of the 128 bits for version information, leaving only 122 bits for the payload (see [here](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))) - and this would cause major problems for us.

Regarding the collisions: we currently also don't see this as a major risk. Our current strategy, as you proposed, is to simply ignore duplicates.
@pithumke Well, you're the one defining both the client and the server side, the EN API is agnostic to its content to allow that freedom of implementation, so you can define that key to be whatever you want it to be, currently you are validating it as UUID in the server and the test case generates a UUIDv4. the UUID data type follows RFC 4122 as you would expect and would naturally cover UUIDv4. All UUID versions reserve a place for version information. _please_ do not try to come up with your own UUID variant if that is what is suggested here
Hi @oiime, thank you for your comment.

> Well, you're the one defining both the client and the server side, the EN API is agnostic to its content to allow that freedom of implementation, so you can define that key to be whatever you want it to be

Yes, we do specify and develop both the CWA server and client. However, we neither specify nor develop the on-device APIs - that is done by Apple and Google. Inside those on-device APIs is where the keys are being generated and that does not give us much freedom regarding data types on our end. In the specification it is stated, that the key data consists of 16 bytes (= 128 bits). Since the use of UUID would restrict that to 122 bits, we can not use it as the column type on postgres.

> currently you are validating it as UUID in the server and the test case generates a UUIDv4

I am not aware of any UUID handling in our current code base, besides for TANs (which are not stored on the server). Can you please link which specific code you are referring to?

> please do not try to come up with your own UUID variant if that is what is suggested here

I didn't mean to suggest that and we don't want to come up with an own UUID variant. This is why we decided to go for a base64 encoded string for now (if bytea turns out to cause problems).
@pithumke 

> However, we neither specify nor develop the on-device API

I guess I rely on looking at the API itself, perhaps apple or google or both have a specific implementation they mandate or suggest, if that is the case then using UUID is wrong anyway 

> I am not aware of any UUID handling in our current code base, besides for TANs

I am likely conflating the two, I have only skimmed through the code and what I recalled was the test generating UUIDv4

> I didn't mean to suggest that and we don't want to come up with an own UUID variant. This is why we decided to go for a base64 encoded string for now (if bytea turns out to cause problems).

base64 seems like an unnecessary step that'll just take up more space and computation, you do it to avoid just storing it in a char(16) as-is? though I don't see why bytea wouldn't work as a PK, especially as you'd never update it

<patch>
diff --git a/common/persistence/pom.xml b/common/persistence/pom.xml
--- a/common/persistence/pom.xml
+++ b/common/persistence/pom.xml
@@ -78,7 +78,7 @@
     <dependency>
       <groupId>com.h2database</groupId>
       <artifactId>h2</artifactId>
-      <version>1.4.199</version>
+      <version>1.4.200</version>
       <scope>runtime</scope>
     </dependency>
     <dependency>
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
@@ -29,9 +29,8 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
+import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
 import javax.persistence.Id;
 import javax.persistence.Table;
 import javax.validation.ConstraintViolation;
@@ -57,10 +56,8 @@ public class DiagnosisKey {
   private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
 
   @Id
-  @GeneratedValue(strategy = GenerationType.IDENTITY)
-  private Long id;
-
   @Size(min = 16, max = 16, message = "Key data must be a byte array of length 16.")
+  @Column(unique = true)
   private byte[] keyData;
 
   @ValidRollingStartIntervalNumber
@@ -100,10 +97,6 @@ public static Builder builder() {
     return new DiagnosisKeyBuilder();
   }
 
-  public Long getId() {
-    return id;
-  }
-
   /**
    * Returns the diagnosis key.
    */
@@ -189,14 +182,13 @@ public boolean equals(Object o) {
         && rollingPeriod == that.rollingPeriod
         && transmissionRiskLevel == that.transmissionRiskLevel
         && submissionTimestamp == that.submissionTimestamp
-        && Objects.equals(id, that.id)
         && Arrays.equals(keyData, that.keyData);
   }
 
   @Override
   public int hashCode() {
     int result = Objects
-        .hash(id, rollingStartIntervalNumber, rollingPeriod, transmissionRiskLevel, submissionTimestamp);
+        .hash(rollingStartIntervalNumber, rollingPeriod, transmissionRiskLevel, submissionTimestamp);
     result = 31 * result + Arrays.hashCode(keyData);
     return result;
   }
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java
@@ -22,6 +22,8 @@
 
 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
 import org.springframework.stereotype.Repository;
 
 @Repository
@@ -33,4 +35,22 @@ public interface DiagnosisKeyRepository extends JpaRepository<DiagnosisKey, Long
    * @param submissionTimestamp the submission timestamp up to which entries will be deleted.
    */
   void deleteBySubmissionTimestampIsLessThanEqual(long submissionTimestamp);
+
+  /**
+   * Attempts to write the specified diagnosis key information into the database. If a row with the specified key data
+   * already exists, no data is inserted.
+   *
+   * @param keyData                    The key data of the diagnosis key.
+   * @param rollingStartIntervalNumber The rolling start interval number of the diagnosis key.
+   * @param rollingPeriod              The rolling period of the diagnosis key.
+   * @param submissionTimestamp        The submission timestamp of the diagnosis key.
+   * @param transmissionRisk           The transmission risk level of the diagnosis key.
+   */
+  @Modifying
+  @Query(nativeQuery = true, value =
+      "INSERT INTO diagnosis_key"
+          + "(key_data, rolling_start_interval_number, rolling_period, submission_timestamp, transmission_risk_level)"
+          + " VALUES(?, ?, ?, ?, ?) ON CONFLICT DO NOTHING;")
+  void saveDoNothingOnConflict(byte[] keyData, int rollingStartIntervalNumber, int rollingPeriod,
+      long submissionTimestamp, int transmissionRisk);
 }
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
@@ -50,13 +50,19 @@ public DiagnosisKeyService(DiagnosisKeyRepository keyRepository) {
   }
 
   /**
-   * Persists the specified collection of {@link DiagnosisKey} instances.
+   * Persists the specified collection of {@link DiagnosisKey} instances. If the key data of a particular diagnosis key
+   * already exists in the database, this diagnosis key is not persisted.
    *
    * @param diagnosisKeys must not contain {@literal null}.
    * @throws IllegalArgumentException in case the given collection contains {@literal null}.
    */
+  @Transactional
   public void saveDiagnosisKeys(Collection<DiagnosisKey> diagnosisKeys) {
-    keyRepository.saveAll(diagnosisKeys);
+    for (DiagnosisKey diagnosisKey : diagnosisKeys) {
+      keyRepository.saveDoNothingOnConflict(
+          diagnosisKey.getKeyData(), diagnosisKey.getRollingStartIntervalNumber(), diagnosisKey.getRollingPeriod(),
+          diagnosisKey.getSubmissionTimestamp(), diagnosisKey.getTransmissionRiskLevel());
+    }
   }
 
   /**
diff --git a/common/persistence/src/main/resources/db/migration/h2/V3__makeKeyDataPrimaryKey.sql b/common/persistence/src/main/resources/db/migration/h2/V3__makeKeyDataPrimaryKey.sql
new file mode 100644
--- /dev/null
+++ b/common/persistence/src/main/resources/db/migration/h2/V3__makeKeyDataPrimaryKey.sql
@@ -0,0 +1,3 @@
+ALTER TABLE diagnosis_key DROP COLUMN id;
+ALTER TABLE diagnosis_key ALTER COLUMN key_data SET NOT NULL;
+ALTER TABLE diagnosis_key ADD PRIMARY KEY (key_data);
\ No newline at end of file
diff --git a/common/persistence/src/main/resources/db/migration/postgres/V3__makeKeyDataPrimaryKey.sql b/common/persistence/src/main/resources/db/migration/postgres/V3__makeKeyDataPrimaryKey.sql
new file mode 100644
--- /dev/null
+++ b/common/persistence/src/main/resources/db/migration/postgres/V3__makeKeyDataPrimaryKey.sql
@@ -0,0 +1,3 @@
+ALTER TABLE diagnosis_key DROP COLUMN id;
+ALTER TABLE diagnosis_key ALTER COLUMN key_data SET NOT NULL;
+ALTER TABLE diagnosis_key ADD PRIMARY KEY (key_data);
\ No newline at end of file

</patch>
diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
--- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
+++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
@@ -33,6 +33,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Random;
 import org.assertj.core.util.Lists;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.DisplayName;
@@ -149,9 +150,34 @@ void testNoPersistOnValidationError() {
     assertDiagnosisKeysEqual(Lists.emptyList(), actKeys);
   }
 
+  @Test
+  void shouldNotUpdateExistingKey() {
+    var keyData = "1234567890123456";
+    var keys = List.of(DiagnosisKey.builder()
+            .withKeyData(keyData.getBytes())
+            .withRollingStartIntervalNumber(600)
+            .withTransmissionRiskLevel(2)
+            .withSubmissionTimestamp(0L).build(),
+        DiagnosisKey.builder()
+            .withKeyData(keyData.getBytes())
+            .withRollingStartIntervalNumber(600)
+            .withTransmissionRiskLevel(3)
+            .withSubmissionTimestamp(0L).build());
+
+    diagnosisKeyService.saveDiagnosisKeys(keys);
+
+    var actKeys = diagnosisKeyService.getDiagnosisKeys();
+
+    assertThat(actKeys.size()).isEqualTo(1);
+    assertThat(actKeys.iterator().next().getTransmissionRiskLevel()).isEqualTo(2);
+  }
+
   public static DiagnosisKey buildDiagnosisKeyForSubmissionTimestamp(long submissionTimeStamp) {
+    byte[] randomBytes = new byte[16];
+    Random random = new Random(submissionTimeStamp);
+    random.nextBytes(randomBytes);
     return DiagnosisKey.builder()
-        .withKeyData(new byte[16])
+        .withKeyData(randomBytes)
         .withRollingStartIntervalNumber(600)
         .withTransmissionRiskLevel(2)
         .withSubmissionTimestamp(submissionTimeStamp).build();
diff --git a/common/persistence/src/test/resources/application.yaml b/common/persistence/src/test/resources/application.yaml
--- a/common/persistence/src/test/resources/application.yaml
+++ b/common/persistence/src/test/resources/application.yaml
@@ -1,5 +1,8 @@
 ---
 spring:
+  test:
+    database:
+      replace: none
   flyway:
     enabled: false
   jpa:
@@ -8,6 +11,10 @@ spring:
     properties:
       hibernate:
         show_sql: false
+  datasource:
+    url: jdbc:h2:mem:test;MODE=PostgreSQL
+    driverClassName: org.h2.Driver
+    platform: h2
   main:
     banner-mode: off
 logging:
diff --git a/services/submission/src/test/resources/application.yaml b/services/submission/src/test/resources/application.yaml
--- a/services/submission/src/test/resources/application.yaml
+++ b/services/submission/src/test/resources/application.yaml
@@ -11,7 +11,13 @@ spring:
     enabled: true
     # default case is H2 - value will be overwritten by profile cloud or postgres
     locations: classpath:db/migration/h2
-
+  datasource:
+    url: jdbc:h2:mem:test;MODE=PostgreSQL
+    driverClassName: org.h2.Driver
+    platform: h2
+  test:
+    database:
+      replace: none
   jpa:
     hibernate:
       ddl-auto: validate
