Commit bfed9529 authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 1.0.3

parent d2f41886
......@@ -26,7 +26,9 @@ object UiConstants {
enum class Notification(val channelId: String, val notificationId: Int) {
AT_RISK("atRisk", 1),
PROXIMITY("proximity", 2),
ERROR("error", 3)
ERROR("error", 3),
UPGRADE("upgrade", 4),
TIME("error", 5)
}
const val DEFAULT_LANGUAGE: String = "en"
......
......@@ -30,14 +30,14 @@ import java.text.Normalizer
import java.util.Locale
@WorkerThread
fun String.download(context: Context): String {
fun String.download(context: Context): okhttp3.Response {
val okHttpClient = OkHttpClient.getDefaultOKHttpClient(context, this, BuildConfig.SERVER_CERTIFICATE_SHA256)
val request: Request = Request.Builder()
.url(this)
.build()
val response = okHttpClient.newCall(request).execute()
return if (response.isSuccessful && response.body != null) {
response.body!!.string()
response
} else {
Timber.d(response.body?.string())
throw HttpException(Response.error<Any>(response.body!!, response))
......@@ -54,7 +54,7 @@ fun String.saveTo(context: Context, file: File) {
if (response.isSuccessful && response.body != null) {
response.body!!.string().byteInputStream().use { input ->
file.outputStream().use { output ->
input.copyTo(output)
input.copyTo(output, 4 * 1024)
}
}
} else {
......
......@@ -14,6 +14,7 @@ import android.content.Context
import androidx.annotation.WorkerThread
import com.lunabeestudio.stopcovid.coreui.BuildConfig
import com.lunabeestudio.stopcovid.coreui.extension.download
import okhttp3.Response
import timber.log.Timber
object ConfigManager {
......@@ -21,7 +22,7 @@ object ConfigManager {
private const val URL: String = BuildConfig.SERVER_URL + BuildConfig.CONFIG_JSON
@WorkerThread
fun fetchLast(context: Context): String {
fun fetchLast(context: Context): Response {
Timber.d("Fetching remote config at $URL")
return URL.download(context)
}
......
......@@ -23,6 +23,7 @@ import java.io.File
import java.lang.reflect.Type
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.abs
abstract class ServerManager {
......@@ -38,9 +39,9 @@ abstract class ServerManager {
protected open fun url(): String = BuildConfig.SERVER_URL
@WorkerThread
protected fun fetchLast(context: Context, languageCode: String): Boolean {
protected fun fetchLast(context: Context, languageCode: String, forceRefresh: Boolean): Boolean {
return try {
if (shouldRefresh(context)) {
if (shouldRefresh(context) || forceRefresh) {
val filename = "${prefix(context)}${languageCode}${extension()}"
Timber.d("Fetching remote data at ${url()}$filename")
"${url()}$filename".saveTo(context, File(context.filesDir, filename))
......@@ -54,7 +55,7 @@ abstract class ServerManager {
Timber.d("Fetching fail for $languageCode")
if (languageCode != UiConstants.DEFAULT_LANGUAGE) {
Timber.d("Trying for ${UiConstants.DEFAULT_LANGUAGE}")
fetchLast(context, UiConstants.DEFAULT_LANGUAGE)
fetchLast(context, UiConstants.DEFAULT_LANGUAGE, forceRefresh)
} else {
false
}
......@@ -93,8 +94,8 @@ abstract class ServerManager {
private fun shouldRefresh(context: Context): Boolean {
return !BuildConfig.USE_LOCAL_DATA
&& System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1L) > PreferenceManager.getDefaultSharedPreferences(context)
.getLong(lastRefreshSharedPrefsKey(), 0L)
&& abs(System.currentTimeMillis() - PreferenceManager.getDefaultSharedPreferences(context)
.getLong(lastRefreshSharedPrefsKey(), 0L)) > TimeUnit.HOURS.toMillis(1L)
}
private fun saveLastRefresh(context: Context) {
......
......@@ -42,7 +42,9 @@ class StringsManager : ServerManager() {
fun appForeground(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
if (StringsManager().fetchLast(context, Locale.getDefault().language) || prevLanguage != Locale.getDefault().language) {
if (StringsManager().fetchLast(context,
Locale.getDefault().language,
prevLanguage != Locale.getDefault().language) || prevLanguage != Locale.getDefault().language) {
prevLanguage = Locale.getDefault().language
_strings = StringsManager().loadLocal(context)
}
......
......@@ -15,7 +15,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/button"
style="@style/Theme.StopCovid.Button"
style="@style/Widget.StopCovid.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
......
......@@ -15,7 +15,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/button"
style="@style/Theme.StopCovid.Button.Light"
style="@style/Widget.StopCovid.Button.Light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
......
......@@ -32,7 +32,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/bottomSheetButton"
style="@style/Theme.StopCovid.Button"
style="@style/Widget.StopCovid.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_xlarge"
......
......@@ -32,7 +32,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/mainButton"
style="@style/Theme.StopCovid.Button"
style="@style/Widget.StopCovid.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
......@@ -43,7 +43,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/dangerButton"
style="@style/Theme.StopCovid.Button.Danger"
style="@style/Widget.StopCovid.Button.Danger"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
......@@ -53,7 +53,7 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/lightButton"
style="@style/Theme.StopCovid.Button.Light"
style="@style/Widget.StopCovid.Button.Light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_large"
......
......@@ -9,13 +9,13 @@
-->
<resources>
<style name="Theme.StopCovid.Button.Light">
<style name="Widget.StopCovid.Button.Light">
<item name="backgroundTint">@color/color_primary_light</item>
<item name="android:textColor">@color/color_on_primary_light</item>
<item name="rippleColor">@color/color_primary</item>
</style>
<style name="Theme.StopCovid.Button.Danger" parent="Theme.StopCovid.Button.Light">
<style name="Widget.StopCovid.Button.Danger" parent="Widget.StopCovid.Button.Light">
<item name="backgroundTint">@color/color_error_light</item>
<item name="rippleColor">@color/color_error</item>
<item name="android:textColor">@color/color_error</item>
......
......@@ -26,6 +26,7 @@
<item name="appBarLayoutStyle">@style/Widget.MaterialComponents.AppBarLayout.Surface</item>
<item name="toolbarStyle">@style/Widget.StopCovid.Toolbar.Surface</item>
<item name="actionMenuTextColor">?colorControlNormal</item>
<item name="snackbarTextViewStyle">@style/Widget.StopCovid.Snackbar.TextView</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
......
......@@ -23,7 +23,7 @@
<item name="android:textColor">?colorOnSurface</item>
</style>
<style name="Theme.StopCovid.Button" parent="Widget.MaterialComponents.Button">
<style name="Widget.StopCovid.Button" parent="Widget.MaterialComponents.Button">
<item name="android:padding">@dimen/spacing_large</item>
<item name="android:textSize">@dimen/button_font_size</item>
<item name="elevation">0dp</item>
......@@ -33,7 +33,7 @@
<item name="rippleColor">@color/color_primary_light</item>
</style>
<style name="Theme.StopCovid.Button.Light" parent="Widget.MaterialComponents.Button.OutlinedButton">
<style name="Widget.StopCovid.Button.Light" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="cornerRadius">@dimen/corner_radius</item>
<item name="android:padding">@dimen/spacing_large</item>
<item name="strokeColor">@color/color_primary_light</item>
......@@ -45,7 +45,7 @@
<item name="android:letterSpacing">@dimen/button_letter_spacing</item>
</style>
<style name="Theme.StopCovid.Button.Danger" parent="Theme.StopCovid.Button.Light">
<style name="Widget.StopCovid.Button.Danger" parent="Widget.StopCovid.Button.Light">
<item name="strokeColor">@color/color_error_light</item>
<item name="elevation">0dp</item>
<item name="android:textAllCaps">false</item>
......@@ -53,7 +53,7 @@
<item name="android:textColor">@color/color_error</item>
</style>
<style name="Theme.StopCovid.TextInput" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<style name="Widget.StopCovid.TextInput" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxCornerRadiusTopStart">@dimen/corner_radius</item>
<item name="boxCornerRadiusTopEnd">@dimen/corner_radius</item>
<item name="boxCornerRadiusBottomStart">@dimen/corner_radius</item>
......@@ -64,4 +64,8 @@
<item name="errorEnabled">true</item>
</style>
<style name="Widget.StopCovid.Snackbar.TextView" parent="@style/Widget.MaterialComponents.Snackbar.TextView">
<item name="android:maxLines">5</item>
</style>
</resources>
\ No newline at end of file
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Lunabee Studio / Date - 2020/09/06 - for the STOP-COVID project
*/
package com.lunabeestudio.framework.local
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import kotlin.random.Random
class LocalCryptoManagerTest {
private lateinit var localCryptoManager: LocalCryptoManager
@Before
fun init() {
val context = ApplicationProvider.getApplicationContext<Context>()
localCryptoManager = LocalCryptoManager(context)
}
@Test
fun encrypt_decrypt_shortByteArray() {
val passphrase = Random.nextBytes(Random.nextInt(0, 4096))
val encrypted = localCryptoManager.encrypt(passphrase.copyOf())
val decrypted = localCryptoManager.decrypt(encrypted)
assertThat(encrypted).isNotEqualTo(passphrase)
assertThat(decrypted).isEqualTo(passphrase)
}
@Test
fun encrypt_decrypt_longByteArray() {
val passphrase = Random.nextBytes(Random.nextInt(4096, 16384))
val encrypted = localCryptoManager.encrypt(passphrase.copyOf())
val decrypted = localCryptoManager.decrypt(encrypted)
assertThat(encrypted).isNotEqualTo(passphrase)
assertThat(decrypted).isEqualTo(passphrase)
}
}
\ No newline at end of file
......@@ -70,6 +70,24 @@ class KeystoreDataSourceTest {
assert(key.contentEquals(decryptedKey!!))
}
@Test
fun saveLongString_and_getLongString() {
val key = Random.nextBytes(8732)
keystoreDataSource.kA = key
val storedString = ApplicationProvider.getApplicationContext<Context>()
.getSharedPreferences("robert_prefs", Context.MODE_PRIVATE)
.getString("shared.pref.ka", null)
assertThat(storedString).isNotNull()
assertThat(storedString).isNotEqualTo(key)
val decryptedKey = keystoreDataSource.kA
assertThat(decryptedKey).isNotNull()
assert(key.contentEquals(decryptedKey!!))
}
@Test
fun saveKA_and_removeKA() {
val key = Random.nextBytes(16)
......
......@@ -18,14 +18,15 @@ import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import com.lunabeestudio.domain.extension.safeUse
import com.lunabeestudio.framework.utils.SelfDestroyCipherInputStream
import com.lunabeestudio.framework.utils.SelfDestroyCipherOutputStream
import com.lunabeestudio.robert.extension.randomize
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.StringWriter
import java.math.BigInteger
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
......@@ -75,7 +76,7 @@ class LocalCryptoManager(private val appContext: Context) {
val tmpFile = createTempFile(directory = targetFile.parentFile)
createCipherOutputStream(tmpFile.outputStream()).use { output ->
clearText.byteInputStream().use { input ->
input.copyTo(output)
input.copyTo(output, BUFFER_SIZE)
}
}
tmpFile.renameTo(targetFile)
......@@ -83,31 +84,20 @@ class LocalCryptoManager(private val appContext: Context) {
@Synchronized
fun encrypt(passphrase: ByteArray, clearPassphrase: Boolean = true): ByteArray {
val iv = ByteArray(AES_GCM_IV_LENGTH)
val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
val ciphertext = localProtectionKey.safeUse { secretKey ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
cipher.iv.copyInto(iv)
} else {
prng.nextBytes(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
val bos = ByteArrayOutputStream()
createCipherOutputStream(bos, false).use { cos ->
passphrase.inputStream().use { input ->
input.copyTo(cos, BUFFER_SIZE)
}
cipher.doFinal(passphrase)
}
val ciphertext = bos.toByteArray()
if (clearPassphrase) {
passphrase.randomize()
}
val encrypted = ByteArray(iv.size + ciphertext.size)
System.arraycopy(iv, 0, encrypted, 0, iv.size)
System.arraycopy(ciphertext, 0, encrypted, iv.size, ciphertext.size)
return encrypted
return ciphertext
}
fun decrypt(encryptedText: String): ByteArray {
......@@ -120,25 +110,22 @@ class LocalCryptoManager(private val appContext: Context) {
val fis = file.inputStream()
val cis = createCipherInputStream(fis)
return cis.reader().use {
it.readText()
return cis.reader().use { reader ->
val buffer = StringWriter()
reader.copyTo(buffer, BUFFER_SIZE)
buffer.toString()
}
}
@Synchronized
fun decrypt(encryptedData: ByteArray): ByteArray {
val iv: ByteArray = encryptedData.copyOfRange(0,
AES_GCM_IV_LENGTH)
val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
val ivSpec = GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv)
return localProtectionKey.safeUse { secretKey ->
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
cipher.doFinal(encryptedData,
AES_GCM_IV_LENGTH, encryptedData.size - AES_GCM_IV_LENGTH)
val bos = ByteArrayOutputStream()
bos.use { output ->
createCipherInputStream(encryptedData.inputStream(), AES_GCM_IV_LENGTH).use { cis ->
cis.copyTo(output, BUFFER_SIZE)
}
}
return bos.toByteArray()
}
/**
......@@ -237,7 +224,7 @@ class LocalCryptoManager(private val appContext: Context) {
NoSuchProviderException::class,
KeyStoreException::class,
IllegalBlockSizeException::class)
fun createCipherOutputStream(outputStream: OutputStream): OutputStream {
fun createCipherOutputStream(outputStream: OutputStream, writeIvSize: Boolean = true): OutputStream {
val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
val iv: ByteArray = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
cipher.init(Cipher.ENCRYPT_MODE, localProtectionKey)
......@@ -249,7 +236,9 @@ class LocalCryptoManager(private val appContext: Context) {
iv
}
outputStream.write(iv.size)
if (writeIvSize) {
outputStream.write(iv.size)
}
outputStream.write(iv)
return SelfDestroyCipherOutputStream(outputStream, cipher, localProtectionKey)
......@@ -259,6 +248,7 @@ class LocalCryptoManager(private val appContext: Context) {
* Create a CipherInputStream instance.
*
* @param inputStream the input stream
* @param pIvLength the length of the IV of null if it should be read at the beginning of the stream
* @return the created InputStream
*/
@Throws(NoSuchPaddingException::class,
......@@ -271,9 +261,9 @@ class LocalCryptoManager(private val appContext: Context) {
NoSuchProviderException::class,
InvalidAlgorithmParameterException::class,
IOException::class)
fun createCipherInputStream(inputStream: InputStream): InputStream {
fun createCipherInputStream(inputStream: InputStream, pIvLength: Int? = null): InputStream {
inputStream.mark(4 + AES_GCM_IV_LENGTH)
val ivLen: Int = inputStream.read()
val ivLen: Int = pIvLength ?: inputStream.read()
val iv = ByteArray(ivLen)
inputStream.read(iv)
val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
......@@ -302,6 +292,8 @@ class LocalCryptoManager(private val appContext: Context) {
private const val RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding"
private const val AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection"
private const val BUFFER_SIZE = 4 * 1024
private val prng: SecureRandom = SecureRandom()
}
}
\ No newline at end of file
......@@ -80,10 +80,10 @@ object RetrofitClient {
}
addInterceptor(getDefaultHeaderInterceptor())
addInterceptor(getLogInterceptor())
callTimeout(1L, TimeUnit.MINUTES)
connectTimeout(1L, TimeUnit.MINUTES)
readTimeout(1L, TimeUnit.MINUTES)
writeTimeout(1L, TimeUnit.MINUTES)
callTimeout(30L, TimeUnit.SECONDS)
connectTimeout(30L, TimeUnit.SECONDS)
readTimeout(30L, TimeUnit.SECONDS)
writeTimeout(30L, TimeUnit.SECONDS)
}.build()
}
......
......@@ -17,4 +17,5 @@ interface RobertApplication {
fun getAppContext(): Context
fun refreshProximityService()
fun atRiskDetected()
fun sendClockNotAlignedNotification()
}
\ No newline at end of file
......@@ -25,6 +25,7 @@ internal object RobertConstant {
const val BLE_SERVICE_UUID: String = "0000fd64-0000-1000-8000-00805f9b34fb"
const val BLE_CHARACTERISTIC_UUID: String = "a8f12d00-ee67-478b-b95f-65d599407756"
const val BLE_BACKGROUND_SERVICE_MANUFACTURER_DATA_IOS: String = "1.0.0.0.0.0.0.0.0.0.0.8.0.0.0.0.0"
const val MIN_GAP_SUCCESS_STATUS: Long = 30L * 60L * 1000L
object CONFIG {
const val DATA_RETENTION_PERIOD: String = "app.dataRetentionPeriod"
......
......@@ -69,7 +69,7 @@ interface RobertManager {
suspend fun eraseLocalHistory(): RobertResult
suspend fun eraseRemoteExposureHistory(): RobertResult
suspend fun eraseRemoteExposureHistory(application: RobertApplication): RobertResult
suspend fun eraseRemoteAlert(): RobertResult
......
......@@ -12,6 +12,7 @@ package com.lunabeestudio.robert
import android.content.Context
import android.util.Base64
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.WorkManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
......@@ -32,6 +33,7 @@ import com.lunabeestudio.robert.datasource.LocalLocalProximityDataSource
import com.lunabeestudio.robert.datasource.RemoteServiceDataSource
import com.lunabeestudio.robert.datasource.SharedCryptoDataSource
import com.lunabeestudio.robert.extension.use
import com.lunabeestudio.robert.model.TimeNotAlignedException
import com.lunabeestudio.robert.model.NoEphemeralBluetoothIdentifierFound
import com.lunabeestudio.robert.model.NoEphemeralBluetoothIdentifierFoundForEpoch
import com.lunabeestudio.robert.model.NoKeyException
......@@ -46,9 +48,8 @@ import com.lunabeestudio.robert.repository.LocalProximityRepository
import com.lunabeestudio.robert.repository.RemoteServiceRepository
import com.lunabeestudio.robert.worker.StatusWorker
import timber.log.Timber
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.math.abs
class RobertManagerImpl(
application: RobertApplication,
......@@ -126,29 +127,30 @@ class RobertManagerImpl(
get() = keystoreRepository.randomStatusHour ?: RobertConstant.RANDOM_STATUS_HOUR
override suspend fun register(application: RobertApplication, captcha: String): RobertResult {
val result = remoteServiceRepository.register(captcha)
return when (result) {
val configResult = remoteServiceRepository.fetchConfig(application.getAppContext())
return when (configResult) {
is RobertResultData.Success -> {
if (result.data.configuration.isNullOrEmpty()) {
val configResult = remoteServiceRepository.fetchConfig(application.getAppContext())
when (configResult) {
if (configResult.data.isNullOrEmpty()) {
clearLocalData(application)
RobertResult.Failure(RobertUnknownException())
} else {
val registerResult = remoteServiceRepository.register(captcha)
when (registerResult) {
is RobertResultData.Success -> {
if (configResult.data.isNullOrEmpty()) {
RobertResult.Failure(RobertUnknownException())
} else {
finishRegister(application, result.data, configResult.data)
}
finishRegister(application, registerResult.data, configResult.data)
}
is RobertResultData.Failure -> {
clearLocalData(application)
RobertResult.Failure(configResult.error)
RobertResult.Failure(registerResult.error)
}
}
} else {
finishRegister(application, result.data, result.data.configuration)
}
}
is RobertResultData.Failure -> RobertResult.Failure(result.error)
is RobertResultData.Failure -> {
clearLocalData(application)
RobertResult.Failure(configResult.error)
}
}
}
......@@ -163,6 +165,7 @@ class RobertManagerImpl(
activateProximity(application)
RobertResult.Success()
} catch (e: Exception) {
Timber.e(e)
clearLocalData(application)
if (e is RobertException) {