From 25406151c191c838bd00be2f395b1f23ba60a8e8 Mon Sep 17 00:00:00 2001
From: Thomas Klingbeil <64434904+tklingbeil@users.noreply.github.com>
Date: Mon, 8 Jun 2020 17:56:38 +0200
Subject: [PATCH] Extend teleTAN to 10 characters + input validation
 (re-submit) (#270)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Change teleTAN from 7 to 10 characters

# Conflicts:
#	Corona-Warn-App/src/main/res/values/dimens.xml

* adjust TAN input to updated design

# Conflicts:
#	Corona-Warn-App/src/main/res/values/dimens.xml

* move TAN to upper case centrally

* styling of filled/empty TAN input spaces

* replace gradient at TAN entry with solid shape

* fix TAN input dash style

* add teleTAN input validation

* ensure valid characters are entered
* block further input if character is invalid
* mark invalid characters accordingly
* show error message if character is invalid
* calculate checksum
* show error and prevent the user from clicking „next“ if checksum is invalid

* code formatting

* Add include to TAN fragment

* Update teleTAN entry info text

* remove code smells

* Change detection of valid characters in TAN to whitelist

* add tests for TanHelper

* add tests for SubmissionTanViewModel

* use ROOT locale and specific charset for string operations

* extend tests for submissionTanViewModel

* override synced translations

Co-authored-by: Jakob Möller <jakob.moeller@sap.com>
---
 .../ui/submission/SubmissionTanViewModel.kt   | 17 +++-
 .../ui/submission/TanConstants.kt             |  2 +-
 .../coronawarnapp/ui/submission/TanInput.kt   | 43 ++++++++-
 .../de/rki/coronawarnapp/util/TanHelper.kt    | 35 ++++++++
 .../formatter/FormatterSubmissionHelper.kt    |  5 ++
 .../src/main/res/drawable/tan_input_digit.xml | 29 +++---
 .../res/drawable/tan_input_digit_entered.xml  |  6 ++
 .../res/drawable/tan_input_digit_error.xml    | 20 +++++
 .../res/layout/fragment_submission_tan.xml    | 28 ++++++
 .../src/main/res/layout/view_tan_input.xml    | 52 ++++++++++-
 .../src/main/res/values-de/strings.xml        |  2 +-
 .../src/main/res/values-en/strings.xml        |  2 +-
 .../src/main/res/values/dimens.xml            |  6 +-
 .../src/main/res/values/strings.xml           |  6 +-
 .../src/main/res/values/styles.xml            |  7 ++
 .../submission/SubmissionTanViewModelTest.kt  | 61 +++++++++++++
 .../rki/coronawarnapp/util/TanHelperTest.kt   | 88 +++++++++++++++++++
 17 files changed, 385 insertions(+), 24 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt
 create mode 100644 Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml
 create mode 100644 Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt
index c18d49a01..cae07743b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations
 import androidx.lifecycle.ViewModel
 import de.rki.coronawarnapp.storage.SubmissionRepository
+import de.rki.coronawarnapp.util.TanHelper
 
 class SubmissionTanViewModel : ViewModel() {
 
@@ -16,7 +17,21 @@ class SubmissionTanViewModel : ViewModel() {
 
     val isValidTanFormat =
         Transformations.map(tan) {
-            it != null && it.length == TanConstants.MAX_LENGTH
+            it != null &&
+            it.length == TanConstants.MAX_LENGTH &&
+            TanHelper.isChecksumValid(it) &&
+            TanHelper.allCharactersValid(it)
+        }
+
+    val tanChecksumValid =
+        Transformations.map(tan) {
+            ((it !== null && it.trim().length == TanConstants.MAX_LENGTH) &&
+                    TanHelper.isChecksumValid(it).not()).not()
+        }
+
+    val tanCharactersValid =
+        Transformations.map(tan) {
+            !((it != null) && TanHelper.allCharactersValid(it).not())
         }
 
     fun storeTeletan() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt
index b6be13be8..644cf1ee0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.ui.submission
 
 object TanConstants {
-    const val MAX_LENGTH = 7
+    const val MAX_LENGTH = 10
     val ALPHA_NUMERIC_CHARS = ('a'..'z').plus('A'..'Z').plus('0'..'9')
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt
index 70cc48b68..b7b02a825 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt
@@ -8,6 +8,9 @@ import android.view.inputmethod.InputMethodManager
 import android.widget.FrameLayout
 import androidx.core.widget.doOnTextChanged
 import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH
+import de.rki.coronawarnapp.util.TanHelper
+import java.util.Locale
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_edittext
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_1
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_2
@@ -16,6 +19,11 @@ import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_4
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_5
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_6
 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_7
+import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_8
+import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_9
+import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_10
+import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_1
+import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_2
 
 class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
 
@@ -30,7 +38,7 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att
             TanConstants.ALPHA_NUMERIC_CHARS.contains(it)
         }
     }
-    private val lengthFilter = InputFilter.LengthFilter(TanConstants.MAX_LENGTH)
+    private var lengthFilter = InputFilter.LengthFilter(MAX_LENGTH)
 
     var listener: ((String?) -> Unit)? = null
 
@@ -41,6 +49,9 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att
 
         tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter)
 
+        dash_view_1.text = "-"
+        dash_view_2.text = "-"
+
         // register listener
         tan_input_edittext.doOnTextChanged { text, _, _, _ -> updateTan(text) }
         setOnClickListener { showKeyboard() }
@@ -56,9 +67,20 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att
         }
     }
 
+    private fun limitLength(length: Int?) {
+        lengthFilter = InputFilter.LengthFilter(if (length != null) length else MAX_LENGTH)
+        tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter)
+    }
+
     private fun updateTan(text: CharSequence?) {
-        this.tan = text?.toString()
+        this.tan = text?.toString()?.toUpperCase(Locale.ROOT)
         updateDigits()
+        tan?.let {
+            limitLength(
+                if (TanHelper.allCharactersValid(it)) null
+                else it.length
+            )
+        }
         notifyListener()
     }
 
@@ -71,9 +93,24 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att
         tan_input_textview_4,
         tan_input_textview_5,
         tan_input_textview_6,
-        tan_input_textview_7
+        tan_input_textview_7,
+        tan_input_textview_8,
+        tan_input_textview_9,
+        tan_input_textview_10
     ).forEachIndexed { i, tanDigit ->
         tanDigit.text = digitAtIndex(i)
+        tanDigit.background =
+            if (digitAtIndex(i) == "")
+                resources.getDrawable(R.drawable.tan_input_digit, null)
+            else if (TanHelper.isTanCharacterValid(digitAtIndex(i)))
+                resources.getDrawable(R.drawable.tan_input_digit_entered, null)
+            else resources.getDrawable(R.drawable.tan_input_digit_error, null)
+
+        tanDigit.setTextColor(
+            if (TanHelper.isTanCharacterValid(digitAtIndex(i)))
+                resources.getColor(R.color.colorTextSemanticNeutral, null)
+            else resources.getColor(R.color.colorTextSemanticRed, null)
+        )
     }
 
     private fun digitAtIndex(index: Int): String = tan?.getOrNull(index)?.toString() ?: ""
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt
new file mode 100644
index 000000000..7fa1562c7
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt
@@ -0,0 +1,35 @@
+package de.rki.coronawarnapp.util
+
+import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.Locale
+
+object TanHelper {
+    private const val VALID_CHARACTERS = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"
+
+    fun isChecksumValid(tan: String): Boolean {
+        if (tan.trim().length != MAX_LENGTH)
+            return false
+        val subTan = tan.substring(0, MAX_LENGTH - 1).toUpperCase(Locale.ROOT)
+        val tanDigest = MessageDigest.getInstance("SHA-256")
+            .digest(subTan.toByteArray(StandardCharsets.US_ASCII))
+        var checkChar = "%02x".format(tanDigest[0])[0]
+        if (checkChar == '0') checkChar = 'G'
+        if (checkChar == '1') checkChar = 'H'
+
+        return checkChar.toUpperCase() == tan.last().toUpperCase()
+    }
+
+    fun allCharactersValid(tan: String): Boolean {
+        for (character in tan) {
+            if (!isTanCharacterValid(character.toString()))
+                return false
+        }
+    return true
+    }
+
+    fun isTanCharacterValid(character: String): Boolean {
+        return VALID_CHARACTERS.contains(character)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
index 22f1b6b6a..7f16f3ed7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
@@ -193,3 +193,8 @@ fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int =
                 deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN &&
                 deviceUiState != DeviceUIState.SUBMITTED_FINAL
     )
+
+fun formatShowTanCharacterError(
+    charactersValid: Boolean,
+    checksumValid: Boolean
+): Int = formatVisibility(checksumValid && !charactersValid)
diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml
index d06cdb920..48e3d0c51 100644
--- a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml
+++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml
@@ -1,11 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <corners android:radius="@dimen/submission_tan_input_digit_radius" />
-    <gradient
-        android:angle="90"
-        android:centerColor="@color/colorSurface2"
-        android:centerX="0.02"
-        android:endColor="@color/colorSurface2"
-        android:startColor="@color/colorTextSemanticNeutral" />
-</shape>
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<item>
+    <shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle">
+        <corners android:radius="@dimen/submission_tan_input_digit_radius" />
+        <solid android:color="@color/colorSurface2" />
+    </shape>
+</item>
+<item
+    android:height="1.5dp"
+    android:gravity="bottom">
+    <shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle">
+        <corners android:bottomLeftRadius="@dimen/submission_tan_input_digit_radius" />
+        <corners android:bottomRightRadius="@dimen/submission_tan_input_digit_radius" />
+        <solid android:color="@color/colorTextSemanticNeutral" />
+    </shape>
+</item>
+</layer-list>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml
new file mode 100644
index 000000000..0550164c0
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/submission_tan_input_digit_radius" />
+    <solid android:color="@color/colorSurface2" />
+</shape>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml
new file mode 100644
index 000000000..01780eeb0
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape xmlns:android="http://schemas.android.com/apk/res/android"
+            android:shape="rectangle">
+            <corners android:radius="@dimen/submission_tan_input_digit_radius" />
+            <solid android:color="@color/colorSurface2" />
+        </shape>
+    </item>
+    <item
+        android:height="1.5dp"
+        android:gravity="bottom">
+        <shape xmlns:android="http://schemas.android.com/apk/res/android"
+            android:shape="rectangle">
+            <corners android:bottomLeftRadius="@dimen/submission_tan_input_digit_radius" />
+            <corners android:bottomRightRadius="@dimen/submission_tan_input_digit_radius" />
+            <solid android:color="@color/colorTextSemanticRed" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml
index c940d7044..4337a0b5e 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_tan.xml
@@ -5,6 +5,7 @@
 
     <data>
 
+        <import type="de.rki.coronawarnapp.util.formatter.FormatterHelper" />
         <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" />
 
         <variable
@@ -54,6 +55,33 @@
                 app:layout_constraintStart_toStartOf="@+id/guideline_start"
                 app:layout_constraintTop_toBottomOf="@+id/submission_tan_body" />
 
+            <TextView
+                android:id="@+id/submission_tan_character_error"
+                style="@style/subtitle"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:visibility="@{FormatterSubmissionHelper.formatShowTanCharacterError(viewmodel.tanCharactersValid, viewmodel.tanChecksumValid)}"
+                android:text="@string/submission_tan_character_error"
+                android:textColor="@color/colorTextSemanticRed"
+                app:layout_constraintEnd_toStartOf="@+id/guideline_end"
+                app:layout_constraintStart_toStartOf="@+id/guideline_start"
+                app:layout_constraintTop_toBottomOf="@id/submission_tan_input" />
+
+            <TextView
+                android:id="@+id/submission_tan_error"
+                style="@style/subtitle"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:visibility="@{FormatterHelper.formatVisibility(!viewmodel.tanChecksumValid)}"
+                android:text="@string/submission_tan_error"
+                android:textColor="@color/colorTextSemanticRed"
+                app:layout_constraintEnd_toStartOf="@+id/guideline_end"
+                app:layout_constraintStart_toStartOf="@+id/guideline_start"
+                app:layout_constraintTop_toBottomOf="@id/submission_tan_input" />
+
+
             <Button
                 android:id="@+id/submission_tan_button_enter"
                 style="@style/buttonPrimary"
diff --git a/Corona-Warn-App/src/main/res/layout/view_tan_input.xml b/Corona-Warn-App/src/main/res/layout/view_tan_input.xml
index 213e9ff71..9083b5628 100644
--- a/Corona-Warn-App/src/main/res/layout/view_tan_input.xml
+++ b/Corona-Warn-App/src/main/res/layout/view_tan_input.xml
@@ -36,17 +36,26 @@
             android:id="@+id/tan_input_textview_3"
             style="@style/tanInputDigit"
             app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_4"
+            app:layout_constraintEnd_toStartOf="@+id/dash_view_1"
             app:layout_constraintStart_toEndOf="@+id/tan_input_textview_2"
             app:layout_constraintTop_toTopOf="parent"
             tools:text="X" />
 
+        <TextView
+            android:id="@+id/dash_view_1"
+            style="@style/tanInputDash"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_4"
+            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_3"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="-" />
+
         <TextView
             android:id="@+id/tan_input_textview_4"
             style="@style/tanInputDigit"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_5"
-            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_3"
+            app:layout_constraintStart_toEndOf="@+id/dash_view_1"
             app:layout_constraintTop_toTopOf="parent"
             tools:text="X" />
 
@@ -63,19 +72,54 @@
             android:id="@+id/tan_input_textview_6"
             style="@style/tanInputDigit"
             app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_7"
+            app:layout_constraintEnd_toStartOf="@+id/dash_view_2"
             app:layout_constraintStart_toEndOf="@+id/tan_input_textview_5"
             app:layout_constraintTop_toTopOf="parent"
             tools:text="X" />
 
+        <TextView
+            android:id="@+id/dash_view_2"
+            style="@style/tanInputDash"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_7"
+            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_6"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="-" />
+
         <TextView
             android:id="@+id/tan_input_textview_7"
             style="@style/tanInputDigit"
             app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_6"
+            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_8"
+            app:layout_constraintStart_toEndOf="@+id/dash_view_2"
             app:layout_constraintTop_toTopOf="parent"
             tools:text="X" />
 
+        <TextView
+            android:id="@+id/tan_input_textview_8"
+            style="@style/tanInputDigit"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_9"
+            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_7"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="X" />
+
+        <TextView
+            android:id="@+id/tan_input_textview_9"
+            style="@style/tanInputDigit"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/tan_input_textview_10"
+            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_8"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="X" />
+
+        <TextView
+            android:id="@+id/tan_input_textview_10"
+            style="@style/tanInputDigit"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/tan_input_textview_9"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="X" />
     </androidx.constraintlayout.widget.ConstraintLayout>
 
 </layout>
diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml
index bb5abba0b..9555649d9 100644
--- a/Corona-Warn-App/src/main/res/values-de/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/strings.xml
@@ -591,7 +591,7 @@
     <!-- XHED: Page title for TAN submission pge -->
     <string name="submission_tan_title">"TAN-Eingabe"</string>
     <!-- YTXT: Body text for the tan submission page -->
-    <string name="submission_tan_body">"Die TAN ist 7-stellig und Groß- und Kleinschreibung muss nicht beachtet werden.\n\nGeben Sie bitte die Ihnen mitgeteilte TAN ein:"</string>
+    <string name="submission_tan_body">"Geben Sie bitte die 10 Stellen der TAN ein, die Ihnen mitgeteilt wurde."</string>
     <!-- XBUT: Submit TAN button -->
     <string name="submission_tan_button_text">"Weiter"</string>
 
diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml
index a52287993..139deb406 100644
--- a/Corona-Warn-App/src/main/res/values-en/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-en/strings.xml
@@ -593,7 +593,7 @@
     <!-- XHED: Page title for TAN submission pge -->
     <string name="submission_tan_title">"TAN entry"</string>
     <!-- YTXT: Body text for the tan submission page -->
-    <string name="submission_tan_body">"The TAN has 7 characters and is case-insensitive.\n\nPlease enter the TAN you were given:"</string>
+    <string name="submission_tan_body">"Please enter the 10 characters of the TAN you were given:"</string>
     <!-- XBUT: Submit TAN button -->
     <string name="submission_tan_button_text">"Next"</string>
 
diff --git a/Corona-Warn-App/src/main/res/values/dimens.xml b/Corona-Warn-App/src/main/res/values/dimens.xml
index e23305dc4..5b3c0277c 100644
--- a/Corona-Warn-App/src/main/res/values/dimens.xml
+++ b/Corona-Warn-App/src/main/res/values/dimens.xml
@@ -82,8 +82,10 @@
     <!-- Submission Tan Input -->
     <dimen name="submission_tan_input_edittext_size">1dp</dimen>
     <dimen name="submission_tan_input_digit_radius">2dp</dimen>
-    <dimen name="submission_tan_input_digit_width">24dp</dimen>
-    <dimen name="submission_tan_input_digit_height">32dp</dimen>
+    <dimen name="submission_tan_input_digit_width">22dp</dimen>
+    <dimen name="submission_tan_input_digit_height">40dp</dimen>
+    <dimen name="submission_tan_input_dash_width">11dp</dimen>
+    <dimen name="submission_tan_input_dash_height">40dp</dimen>
 
     <!-- Submission QR Code Scan -->
     <dimen name="submission_scan_qr_code_viewfinder_size">240dp</dimen>
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index 0b6468412..ead09a21a 100644
--- a/Corona-Warn-App/src/main/res/values/strings.xml
+++ b/Corona-Warn-App/src/main/res/values/strings.xml
@@ -2664,9 +2664,13 @@ as modifying the License.
     <!-- XHED: Page title for TAN submission pge -->
     <string name="submission_tan_title">TAN Eingabe</string>
     <!-- YTXT: Body text for the tan submission page -->
-    <string name="submission_tan_body">Die TAN ist 7-stellig und Groß- und Kleinschreibung muss beachtet werden.\n\nGeben Sie bitte die Ihnen mitgeteilte TAN ein:</string>
+    <string name="submission_tan_body">Geben Sie bitte die 10 Stellen der TAN ein, die Ihnen mitgeteilt wurde.</string>
     <!-- XBUT: Submit TAN button -->
     <string name="submission_tan_button_text">Weiter</string>
+    <!-- YTXT: Error text for the tan submission page -->
+    <string name="submission_tan_error">Ungültige TAN, bitte überprüfen Sie Ihre Eingabe.</string>
+    <!-- YTXT: Error text for the tan submission page (wrong characters) -->
+    <string name="submission_tan_character_error">Ungültige Eingabe, bitte überprüfen Sie das Zeichen.</string>
 
     <!-- Submission Intro -->
     <!-- XHED: Page title for menu at the start of the submission process  -->
diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml
index 8ea177f88..403916355 100644
--- a/Corona-Warn-App/src/main/res/values/styles.xml
+++ b/Corona-Warn-App/src/main/res/values/styles.xml
@@ -219,6 +219,13 @@
         <item name="android:gravity">center</item>
     </style>
 
+    <style name="tanInputDash" parent="headline6">
+        <item name="android:layout_width">@dimen/submission_tan_input_dash_width</item>
+        <item name="android:layout_height">@dimen/submission_tan_input_dash_height</item>
+        <item name="android:background">@null</item>
+        <item name="android:gravity">center</item>
+    </style>
+
     <style name="tanInputEdittext">
         <item name="android:layout_width">@dimen/submission_tan_input_edittext_size</item>
         <item name="android:layout_height">@dimen/submission_tan_input_edittext_size</item>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt
new file mode 100644
index 000000000..fe08e5de2
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModelTest.kt
@@ -0,0 +1,61 @@
+package de.rki.coronawarnapp.ui.submission
+
+import com.google.android.gms.nearby.exposurenotification.ExposureSummary
+import de.rki.coronawarnapp.storage.SubmissionRepository
+import de.rki.coronawarnapp.util.TanHelper
+import io.mockk.*
+import org.junit.Assert.*
+import org.junit.Test
+
+class SubmissionTanViewModelTest {
+    private var viewModel: SubmissionTanViewModel = SubmissionTanViewModel()
+
+    @Test
+    fun allCharactersValid() {
+        viewModel.tan.postValue("ABCD")
+        viewModel.tanCharactersValid.value?.let { assertTrue(it) }
+
+        viewModel.tan.postValue("ABCD0")
+        viewModel.tanCharactersValid.value?.let { assertFalse(it) }
+
+    }
+
+    @Test
+    fun tanFormatValid() {
+        viewModel.tan.postValue("ZWFPC7NG47")
+        viewModel.isValidTanFormat.value?.let { assertTrue(it) }
+
+        viewModel.tan.postValue("ABC")
+        viewModel.isValidTanFormat.value?.let { assertFalse(it) }
+
+        viewModel.tan.postValue("ZWFPC7NG48")
+        viewModel.isValidTanFormat.value?.let { assertFalse(it) }
+
+        viewModel.tan.postValue("ZWFPC7NG4A")
+        viewModel.isValidTanFormat.value?.let { assertFalse(it) }
+    }
+
+    @Test
+    fun checksumValid() {
+        viewModel.tan.postValue("ZWFPC7NG47")
+        viewModel.tanChecksumValid.value?.let { assertTrue(it) }
+
+        viewModel.tan.postValue("ZWFPC7NG48")
+        viewModel.tanChecksumValid.value?.let { assertFalse(it) }
+    }
+
+    @Test
+    fun testTanStorage() {
+        val sr = mockk<SubmissionRepository> {
+            every { setTeletan(any()) } just Runs
+        }
+        val tan = "ZWFPC7NG47";
+        sr.setTeletan(tan)
+
+        verify (exactly = 1) { sr.setTeletan(
+            withArg {
+                assertEquals(it, tan)
+            })
+        }
+    }
+}
\ No newline at end of file
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt
new file mode 100644
index 000000000..d3c4a1120
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TanHelperTest.kt
@@ -0,0 +1,88 @@
+package de.rki.coronawarnapp.util
+
+import de.rki.coronawarnapp.service.submission.SubmissionService
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Test
+
+class TanHelperTest {
+
+    @Test
+    fun isValidCharacter() {
+        // valid
+        val validCharacters = arrayOf(
+            "2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H",
+            "J","K","M","N","P","Q","R","S","T","U","V","W","X","Y","Z"
+        )
+        for (character in validCharacters) {
+            MatcherAssert.assertThat(
+                TanHelper.isTanCharacterValid(character),
+                CoreMatchers.equalTo(true)
+            )
+        }
+
+        // invalid
+        val invalidCharacters = arrayOf(
+            "0","1","O","L","I","Ö","*","&","-","a","b",
+            "c","ö","ß","é","."," ","€","(",")",";",","
+        )
+        for (character in invalidCharacters) {
+            MatcherAssert.assertThat(
+                TanHelper.isTanCharacterValid(character),
+                CoreMatchers.equalTo(false)
+            )
+        }
+    }
+
+    @Test
+    fun areCharactersValid() {
+        // valid input strings (not necessarily valid TANs)
+        val validStrings = arrayOf(
+            "ABCD", "2345", "PTPHM35RP4", "AAAAAAAAAA", "BBBBB")
+        for (text in validStrings) {
+            MatcherAssert.assertThat(
+                TanHelper.allCharactersValid(text),
+                CoreMatchers.equalTo(true)
+            )
+        }
+
+        // invalid input strings
+        val invalidStrings = arrayOf(
+            "ABCDÖ", "01234", "PTPHM15RP4", "AAAAAA AAA", "BB.BBB")
+        for (text in invalidStrings) {
+            MatcherAssert.assertThat(
+                TanHelper.allCharactersValid(text),
+                CoreMatchers.equalTo(false)
+            )
+        }
+    }
+
+    @Test
+    fun isChecksumValid() {
+        // valid
+        val validTans = arrayOf(
+            "9A3B578UMG", "DEU7TKSV3H", "PTPHM35RP4", "V923D59AT8", "H9NC5CQ34E")
+        for (tan in validTans) {
+            MatcherAssert.assertThat(
+                TanHelper.isChecksumValid(tan),
+                CoreMatchers.equalTo(true)
+            )
+        }
+
+        // invalid
+        val invalidTans = arrayOf(
+            "DEU7TKSV32", "DEU7TKSV33", "DEU7TKSV34", "DEU7TKSV35",
+            "DEU7TKSV36", "DEU7TKSV37", "DEU7TKSV38", "DEU7TKSV39",
+            "DEU7TKSV3A", "DEU7TKSV3B", "DEU7TKSV3C", "DEU7TKSV3D",
+            "DEU7TKSV3E", "DEU7TKSV3F", "DEU7TKSV3G",
+            " QV5FQ38MA",
+            "9A3B578UM0", "DEU7TKSV31", "Q4XBJCB43", "929B96CA8"
+        )
+        for (tan in invalidTans) {
+            MatcherAssert.assertThat(
+                TanHelper.isChecksumValid(tan),
+                CoreMatchers.equalTo(false)
+            )
+        }
+    }
+}
\ No newline at end of file
-- 
GitLab