COVIDSafe code from version 1.0.21 (#3)

This commit is contained in:
COVIDSafe Support 2020-06-05 10:24:35 +10:00 committed by GitHub
parent 3b77cc31e5
commit 05a2ca94a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1558 additions and 112 deletions

92
.gitignore vendored Normal file
View file

@ -0,0 +1,92 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
.idea/misc.xml
.idea/Project.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
.idea
.DS_Store
AndroidSqlEditorDatabases

View file

@ -34,14 +34,14 @@ android {
defaultConfig { defaultConfig {
applicationId "au.gov.health.covidsafe" applicationId "au.gov.health.covidsafe"
resValue "string", "build_config_package", "au.gov.health.covidsafe" resValue "string", "build_config_package", "au.gov.health.covidsafe"
minSdkVersion 23 minSdkVersion 21
/* /*
TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29 TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29, in order to fix a BLE vulnerability
Before you increase the targetSdkVersion make sure that all its usage are still working Before you increase the targetSdkVersion make sure that all its usage are still working
*/ */
targetSdkVersion 28 targetSdkVersion 28
versionCode 18 versionCode 21
versionName "1.0.18" versionName "1.0.21"
buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" buildConfigField "String", "GITHASH", "\"${getGitHash()}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -238,7 +238,8 @@ dependencies {
implementation 'com.airbnb.android:lottie:3.4.0' implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'com.google.guava:guava:28.2-android' implementation 'com.google.guava:guava:28.2-android'
implementation "androidx.security:security-crypto:1.0.0-beta01" implementation "androidx.collection:collection:1.1.0"
implementation "com.google.crypto.tink:tink-android:1.4.0-rc2"
implementation "androidx.lifecycle:lifecycle-service:2.2.0" implementation "androidx.lifecycle:lifecycle-service:2.2.0"
implementation 'com.github.razir.progressbutton:progressbutton:2.0.1' implementation 'com.github.razir.progressbutton:progressbutton:2.0.1'

View file

@ -1,8 +1,10 @@
package au.gov.health.covidsafe package au.gov.health.covidsafe
import android.content.Context import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences import android.os.Build
import androidx.security.crypto.MasterKeys import au.gov.health.covidsafe.security.crypto.EncryptedSharedPreferences
import au.gov.health.covidsafe.security.crypto.MasterKeys
import au.gov.health.covidsafe.security.crypto.AESEncryptionForPreAndroidM
object Preference { object Preference {
private const val PREF_ID = "Tracer_pref" private const val PREF_ID = "Tracer_pref"
@ -10,6 +12,8 @@ object Preference {
private const val PHONE_NUMBER = "PHONE_NUMBER" private const val PHONE_NUMBER = "PHONE_NUMBER"
private const val HANDSHAKE_PIN = "HANDSHAKE_PIN" private const val HANDSHAKE_PIN = "HANDSHAKE_PIN"
private const val DEVICE_ID = "DEVICE_ID" private const val DEVICE_ID = "DEVICE_ID"
private const val ENCRYPTED_AES_KEY = "ENCRYPTED_AES_KEY"
private const val AES_IV = "AES_IV"
private const val JWT_TOKEN = "JWT_TOKEN" private const val JWT_TOKEN = "JWT_TOKEN"
private const val IS_DATA_UPLOADED = "IS_DATA_UPLOADED" private const val IS_DATA_UPLOADED = "IS_DATA_UPLOADED"
private const val DATA_UPLOADED_DATE_MS = "DATA_UPLOADED_DATE_MS" private const val DATA_UPLOADED_DATE_MS = "DATA_UPLOADED_DATE_MS"
@ -32,8 +36,29 @@ object Preference {
?.getString(DEVICE_ID, "") ?: "" ?.getString(DEVICE_ID, "") ?: ""
} }
fun putEncodedAESInitialisationVector(context: Context, value: String) {
context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
.edit().putString(AES_IV, value)?.apply()
}
fun getEncodedAESInitialisationVector(context: Context) : String? {
return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
.getString(AES_IV, null)
}
fun putEncodedRSAEncryptedAESKey(context: Context, value: String) {
context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
.edit().putString(ENCRYPTED_AES_KEY, value)?.apply()
}
fun getEncodedRSAEncryptedAESKey(context: Context): String? {
return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
?.getString(ENCRYPTED_AES_KEY, null)
}
fun putEncrypterJWTToken(context: Context?, jwtToken: String?) { fun putEncrypterJWTToken(context: Context?, jwtToken: String?) {
context?.let { context?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
PREF_ID, PREF_ID,
@ -42,11 +67,21 @@ object Preference {
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
).edit()?.putString(JWT_TOKEN, jwtToken)?.apply() ).edit()?.putString(JWT_TOKEN, jwtToken)?.apply()
} else {
val aesEncryptedJwtToken = jwtToken?.let {
AESEncryptionForPreAndroidM.encrypt(it)
}
context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
.edit().putString(JWT_TOKEN, aesEncryptedJwtToken)?.apply()
}
} }
} }
fun getEncrypterJWTToken(context: Context?): String? { fun getEncrypterJWTToken(context: Context?): String? {
return context?.let { return context?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
PREF_ID, PREF_ID,
@ -55,6 +90,12 @@ object Preference {
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
).getString(JWT_TOKEN, null) ).getString(JWT_TOKEN, null)
} else {
context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
?.getString(JWT_TOKEN, null)?.let {
AESEncryptionForPreAndroidM.decrypt(it)
}
}
} }
} }

View file

@ -3,11 +3,33 @@ package au.gov.health.covidsafe.extensions
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
fun Context.isInternetAvailable(): Boolean { fun Context.isInternetAvailable(): Boolean {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
val capabilities = connectivityManager?.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
return capabilities != null && (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
} else{
@Suppress("DEPRECATION")
val allNetworkInfo = connectivityManager.allNetworkInfo
for (networkInfo in allNetworkInfo) {
@Suppress("DEPRECATION")
if(networkInfo.isConnected){
when(networkInfo.type){
ConnectivityManager.TYPE_MOBILE -> return true
ConnectivityManager.TYPE_WIFI -> return true
ConnectivityManager.TYPE_ETHERNET -> return true
}
}
}
return false
}
} }

View file

@ -1,11 +1,14 @@
package au.gov.health.covidsafe.factory package au.gov.health.covidsafe.factory
import android.os.Build
import au.gov.health.covidsafe.BuildConfig
import au.gov.health.covidsafe.networking.service.AwsClient
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import au.gov.health.covidsafe.BuildConfig
import au.gov.health.covidsafe.networking.service.AwsClient
interface NetworkFactory { interface NetworkFactory {
companion object { companion object {
@ -17,11 +20,37 @@ interface NetworkFactory {
} }
val okHttpClient: OkHttpClient by lazy { val okHttpClient: OkHttpClient by lazy {
val builder = OkHttpClient.Builder() val okHttpClientBuilder = OkHttpClient.Builder()
if (!builder.interceptors().contains(logging) && BuildConfig.DEBUG) {
builder.addInterceptor(logging) if (!okHttpClientBuilder.interceptors().contains(logging) && BuildConfig.DEBUG) {
okHttpClientBuilder.addInterceptor(logging)
} }
builder.build()
// This certificate pinning mechanism is only needed on Android 23 and lower.
// For Android 24 and above, the pinning is set up in AndroidManifest.xml
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
// '**' in '**.aws.covidsafe.gov.au' is to include all sub domains
val hostPattern = "**.aws.covidsafe.gov.au"
val amazonRootCa1Sha256 = "sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI="
val amazonRootCa2Sha256 = "sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE="
val amazonRootCa3Sha256 = "sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k="
val amazonRootCa4Sha256 = "sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U="
val sfsRootCag2Sha256 = "sha256/KwccWaCgrnaw6tsrrSO61FgLacNgG2MMLq8GE6+oP5I="
val certificatePinner = CertificatePinner.Builder()
.add(hostPattern,
amazonRootCa1Sha256,
amazonRootCa2Sha256,
amazonRootCa3Sha256,
amazonRootCa4Sha256,
sfsRootCag2Sha256
)
.build()
okHttpClientBuilder.certificatePinner(certificatePinner)
}
okHttpClientBuilder.build()
} }
} }
} }

View file

@ -0,0 +1,213 @@
package au.gov.health.covidsafe.security.crypto
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import au.gov.health.covidsafe.Preference
import au.gov.health.covidsafe.TracerApp
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.Key
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.security.auth.x500.X500Principal
/**
* This object provides AES encryption and decryption, with the AES key stored in an RSA encrypted
* format. The RSA keys are in turn stored in Android KeyStore. In this way, even if hackers get
* the AES encrypted key from Shared Preference, they wouldn't be able to decode and use them,
* because they can't get the RSA keys.
*
* This object should be used for pre Android M (API 23). For Android M and above, use MasterKeys
* instead to generate and store the AES key into Android KeyStore directly.
*/
object AESEncryptionForPreAndroidM {
// keystore: for storing RSA keys
private val ANDROID_KEY_STORE = "AndroidKeyStore"
private val RSA_KEY_ALIAS = "RSA_KEY_ALIAS"
private val RSA_MODE = "RSA/ECB/PKCS1Padding"
private val AES_MODE = "AES/CBC/PKCS5Padding"
lateinit var keyStore: KeyStore
init {
generateAndStoreRSAKeyPairs()
generateEncryptAndStoreAESKey()
}
/**
* Generate RSA key pairs and store them into the Android KeyStore
*/
private fun generateAndStoreRSAKeyPairs() {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
if (!keyStore.containsAlias(RSA_KEY_ALIAS)) {
// Generate a key pair for encryption
val start: Calendar = Calendar.getInstance()
val end: Calendar = Calendar.getInstance()
end.add(Calendar.YEAR, 1)
val spec = KeyPairGeneratorSpec.Builder(TracerApp.AppContext)
.setAlias(RSA_KEY_ALIAS)
.setSubject(X500Principal("CN=$RSA_KEY_ALIAS"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.time)
.setEndDate(end.time)
.setKeySize(2048)
.build()
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_RSA,
ANDROID_KEY_STORE
)
kpg.initialize(spec)
kpg.generateKeyPair()
}
}
/**
* This function is used to encrypt the AES key
*/
private fun rsaEncrypt(plainBytes: ByteArray): ByteArray {
val privateKeyEntry = keyStore.getEntry(RSA_KEY_ALIAS, null)
as KeyStore.PrivateKeyEntry
val inputCipher: Cipher = Cipher.getInstance(RSA_MODE)
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
cipherOutputStream.write(plainBytes)
cipherOutputStream.close()
return outputStream.toByteArray()
}
/**
* This function is used to decrypt the AES key
*/
private fun rsaDecrypt(encrypted: ByteArray): ByteArray {
val privateKeyEntry = keyStore.getEntry(RSA_KEY_ALIAS, null)
as KeyStore.PrivateKeyEntry
val outputCipher = Cipher.getInstance(RSA_MODE)
outputCipher.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
val cipherInputStream = CipherInputStream(ByteArrayInputStream(encrypted), outputCipher)
val values: ArrayList<Byte> = ArrayList()
var nextByte: Int
while (cipherInputStream.read().also { nextByte = it } != -1) {
values.add(nextByte.toByte())
}
val bytes = ByteArray(values.size)
for (i in bytes.indices) {
bytes[i] = values[i]
}
return bytes
}
/**
* Generate an AES key and encrypt it by RSA
*/
private fun generateEncryptAndStoreAESKey() {
var encodedRSAEncryptedAESKey = Preference.getEncodedRSAEncryptedAESKey(TracerApp.AppContext)
if (encodedRSAEncryptedAESKey == null) {
// generate an AES key and iv
val secureRandom = SecureRandom()
val key = ByteArray(16)
secureRandom.nextBytes(key)
val iv = ByteArray(16)
secureRandom.nextBytes(iv)
// the IV is stored into Shared Preferences
Preference.putEncodedAESInitialisationVector(
TracerApp.AppContext,
Base64.encodeToString(
iv,
Base64.DEFAULT
)
)
// encrypt it with RSA
val encryptedKey = rsaEncrypt(key)
// encode the RSA encrypted AES key into Base64
encodedRSAEncryptedAESKey = Base64.encodeToString(encryptedKey, Base64.DEFAULT)
// store it into shared preference
Preference.putEncodedRSAEncryptedAESKey(TracerApp.AppContext, encodedRSAEncryptedAESKey)
}
}
/**
* Get the RSA encrypted AES key from shared preferences and decrypt it
*/
private fun getAESKeyFromSharedPreferences(): Key {
Preference.getEncodedRSAEncryptedAESKey(TracerApp.AppContext)?.let {
// decode base64
val rsaEncryptedAESKey = Base64.decode(it, Base64.DEFAULT)
// decrypt the key
val aesKey = rsaDecrypt(rsaEncryptedAESKey)
return SecretKeySpec(aesKey, "AES")
}
throw IllegalStateException("Encrypted AES Key not available in shared preferences.")
}
/**
* Encrypt a string with AES
*/
fun encrypt(plainText: String): String {
Preference.getEncodedAESInitialisationVector(TracerApp.AppContext)?.let {
val iv = Base64.decode(it, Base64.DEFAULT)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(
Cipher.ENCRYPT_MODE,
getAESKeyFromSharedPreferences(),
IvParameterSpec(iv)
)
val encodedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
return Base64.encodeToString(encodedBytes, Base64.DEFAULT)
}
throw IllegalStateException("AES IV not available in shared preferences.")
}
/**
* Decrypt a string with AES
*/
fun decrypt(aesEncryptedText: String): String {
Preference.getEncodedAESInitialisationVector(TracerApp.AppContext)?.let {
val iv = Base64.decode(it, Base64.DEFAULT)
val encryptedBytes = Base64.decode(aesEncryptedText, Base64.DEFAULT)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(
Cipher.DECRYPT_MODE,
getAESKeyFromSharedPreferences(),
IvParameterSpec(iv)
)
return cipher.doFinal(encryptedBytes).toString(Charsets.UTF_8)
}
throw IllegalStateException("AES IV not available in shared preferences.")
}
}

View file

@ -0,0 +1,340 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 for the specific language governing permissions and
* limitations under the License.
*/
package au.gov.health.covidsafe.security.crypto;
import static au.gov.health.covidsafe.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.crypto.tink.KeyTemplate;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.StreamingAead;
import com.google.crypto.tink.config.TinkConfig;
import com.google.crypto.tink.integration.android.AndroidKeysetManager;
import com.google.crypto.tink.streamingaead.AesGcmHkdfStreamingKeyManager;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.security.GeneralSecurityException;
/**
* Class used to create and read encrypted files.
*
* <pre>
* String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
*
* File file = new File(context.getFilesDir(), "secret_data");
* EncryptedFile encryptedFile = EncryptedFile.Builder(
* file,
* context,
* masterKeyAlias,
* EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
* ).build();
*
* // write to the encrypted file
* FileOutputStream encryptedOutputStream = encryptedFile.openFileOutput();
*
* // read the encrypted file
* FileInputStream encryptedInputStream = encryptedFile.openFileInput();
* </pre>
*
*/
public final class EncryptedFile {
private static final String KEYSET_PREF_NAME =
"__androidx_security_crypto_encrypted_file_pref__";
private static final String KEYSET_ALIAS =
"__androidx_security_crypto_encrypted_file_keyset__";
final File mFile;
final Context mContext;
final String mMasterKeyAlias;
final StreamingAead mStreamingAead;
EncryptedFile(
@NonNull File file,
@NonNull String masterKeyAlias,
@NonNull StreamingAead streamingAead,
@NonNull Context context) {
mFile = file;
mContext = context;
mMasterKeyAlias = masterKeyAlias;
mStreamingAead = streamingAead;
}
/**
* The encryption scheme to encrypt files.
*/
public enum FileEncryptionScheme {
/**
* The file content is encrypted using
* <a href="https://google.github.io/tink/javadoc/tink/1.4.0-rc2/com/google/crypto/tink/streamingaead/StreamingAead.html">StreamingAead</a> with AES-GCM, with the
* file name as associated data.
*
* For more information please see the Tink documentation:
*
* <a href="https://google.github.io/tink/javadoc/tink/1.4.0-rc2/com/google/crypto/tink/streamingaead/AesGcmHkdfStreamingKeyManager.html">AesGcmHkdfStreamingKeyManager</a>.aes256GcmHkdf4KBTemplate()
*/
AES256_GCM_HKDF_4KB(AesGcmHkdfStreamingKeyManager.aes256GcmHkdf4KBTemplate());
private final KeyTemplate mStreamingAeadKeyTemplate;
FileEncryptionScheme(KeyTemplate keyTemplate) {
mStreamingAeadKeyTemplate = keyTemplate;
}
KeyTemplate getKeyTemplate() {
return mStreamingAeadKeyTemplate;
}
}
/**
* Builder class to configure EncryptedFile
*/
public static final class Builder {
public Builder(@NonNull File file,
@NonNull Context context,
@NonNull String masterKeyAlias,
@NonNull FileEncryptionScheme fileEncryptionScheme) {
mFile = file;
mFileEncryptionScheme = fileEncryptionScheme;
mContext = context;
mMasterKeyAlias = masterKeyAlias;
}
// Required parameters
File mFile;
final FileEncryptionScheme mFileEncryptionScheme;
final Context mContext;
final String mMasterKeyAlias;
// Optional parameters
String mKeysetPrefName = KEYSET_PREF_NAME;
String mKeysetAlias = KEYSET_ALIAS;
/**
* @param keysetPrefName The SharedPreferences file to store the keyset.
* @return This Builder
*/
@NonNull
public Builder setKeysetPrefName(@NonNull String keysetPrefName) {
mKeysetPrefName = keysetPrefName;
return this;
}
/**
* @param keysetAlias The alias in the SharedPreferences file to store the keyset.
* @return This Builder
*/
@NonNull
public Builder setKeysetAlias(@NonNull String keysetAlias) {
mKeysetAlias = keysetAlias;
return this;
}
/**
* @return An EncryptedFile with the specified parameters.
*/
@NonNull
public EncryptedFile build() throws GeneralSecurityException, IOException {
TinkConfig.register();
KeysetHandle streadmingAeadKeysetHandle = new AndroidKeysetManager.Builder()
.withKeyTemplate(mFileEncryptionScheme.getKeyTemplate())
.withSharedPref(mContext, mKeysetAlias, mKeysetPrefName)
.withMasterKeyUri(KEYSTORE_PATH_URI + mMasterKeyAlias)
.build().getKeysetHandle();
StreamingAead streamingAead =
streadmingAeadKeysetHandle.getPrimitive(StreamingAead.class);
return new EncryptedFile(mFile, mKeysetAlias, streamingAead, mContext);
}
}
/**
* Opens a FileOutputStream for writing that automatically encrypts the data based on the
* provided settings.
*
* Please ensure that the same master key and keyset are used to decrypt or it
* will cause failures.
*
* @return The FileOutputStream that encrypts all data.
* @throws GeneralSecurityException when a bad master key or keyset has been used
* @throws IOException when the file already exists or is not available for writing
*/
@NonNull
public FileOutputStream openFileOutput()
throws GeneralSecurityException, IOException {
if (mFile.exists()) {
throw new IOException("output file already exists, please use a new file: "
+ mFile.getName());
}
FileOutputStream fileOutputStream = new FileOutputStream(mFile);
OutputStream encryptingStream = mStreamingAead.newEncryptingStream(fileOutputStream,
mFile.getName().getBytes(UTF_8));
return new EncryptedFileOutputStream(fileOutputStream.getFD(), encryptingStream);
}
/**
* Opens a FileInputStream that reads encrypted files based on the previous settings.
*
* Please ensure that the same master key and keyset are used to decrypt or it
* will cause failures.
*
* @return The input stream to read previously encrypted data.
* @throws GeneralSecurityException when a bad master key or keyset has been used
* @throws IOException when the file was not found
*/
@NonNull
public FileInputStream openFileInput()
throws GeneralSecurityException, IOException {
if (!mFile.exists()) {
throw new IOException("file doesn't exist: " + mFile.getName());
}
FileInputStream fileInputStream = new FileInputStream(mFile);
InputStream decryptingStream = mStreamingAead.newDecryptingStream(fileInputStream,
mFile.getName().getBytes(UTF_8));
return new EncryptedFileInputStream(fileInputStream.getFD(), decryptingStream);
}
/**
* Encrypted file output stream
*
*/
private static final class EncryptedFileOutputStream extends FileOutputStream {
private final OutputStream mEncryptedOutputStream;
EncryptedFileOutputStream(FileDescriptor descriptor, OutputStream encryptedOutputStream) {
super(descriptor);
mEncryptedOutputStream = encryptedOutputStream;
}
@Override
public void write(@NonNull byte[] b) throws IOException {
mEncryptedOutputStream.write(b);
}
@Override
public void write(int b) throws IOException {
mEncryptedOutputStream.write(b);
}
@Override
public void write(@NonNull byte[] b, int off, int len) throws IOException {
mEncryptedOutputStream.write(b, off, len);
}
@Override
public void close() throws IOException {
mEncryptedOutputStream.close();
}
@NonNull
@Override
public FileChannel getChannel() {
throw new UnsupportedOperationException("For encrypted files, please open the "
+ "relevant FileInput/FileOutputStream.");
}
@Override
public void flush() throws IOException {
mEncryptedOutputStream.flush();
}
}
/**
* Encrypted file input stream
*/
private static final class EncryptedFileInputStream extends FileInputStream {
private final InputStream mEncryptedInputStream;
EncryptedFileInputStream(FileDescriptor descriptor,
InputStream encryptedInputStream) {
super(descriptor);
mEncryptedInputStream = encryptedInputStream;
}
@Override
public int read() throws IOException {
return mEncryptedInputStream.read();
}
@Override
public int read(@NonNull byte[] b) throws IOException {
return mEncryptedInputStream.read(b);
}
@Override
public int read(@NonNull byte[] b, int off, int len) throws IOException {
return mEncryptedInputStream.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return mEncryptedInputStream.skip(n);
}
@Override
public int available() throws IOException {
return mEncryptedInputStream.available();
}
@Override
public void close() throws IOException {
mEncryptedInputStream.close();
}
@Override
public FileChannel getChannel() {
throw new UnsupportedOperationException("For encrypted files, please open the "
+ "relevant FileInput/FileOutputStream.");
}
@Override
public synchronized void mark(int readlimit) {
mEncryptedInputStream.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
mEncryptedInputStream.reset();
}
@Override
public boolean markSupported() {
return mEncryptedInputStream.markSupported();
}
}
}

View file

@ -0,0 +1,600 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 for the specific language governing permissions and
* limitations under the License.
*/
package au.gov.health.covidsafe.security.crypto;
import static au.gov.health.covidsafe.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.ArraySet;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.DeterministicAead;
import com.google.crypto.tink.KeyTemplate;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.aead.AesGcmKeyManager;
import com.google.crypto.tink.config.TinkConfig;
import com.google.crypto.tink.daead.AesSivKeyManager;
import com.google.crypto.tink.integration.android.AndroidKeysetManager;
import com.google.crypto.tink.subtle.Base64;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* An implementation of {@link SharedPreferences} that encrypts keys and values.
*
* <pre>
* String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
*
* SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
* "secret_shared_prefs",
* masterKeyAlias,
* context,
* EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
* EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
* );
*
* // use the shared preferences and editor as you normally would
* SharedPreferences.Editor editor = sharedPreferences.edit();
* </pre>
*/
public final class EncryptedSharedPreferences implements SharedPreferences {
private static final String KEY_KEYSET_ALIAS =
"__androidx_security_crypto_encrypted_prefs_key_keyset__";
private static final String VALUE_KEYSET_ALIAS =
"__androidx_security_crypto_encrypted_prefs_value_keyset__";
private static final String NULL_VALUE = "__NULL__";
final SharedPreferences mSharedPreferences;
final List<OnSharedPreferenceChangeListener> mListeners;
final String mFileName;
final String mMasterKeyAlias;
final Aead mValueAead;
final DeterministicAead mKeyDeterministicAead;
EncryptedSharedPreferences(@NonNull String name,
@NonNull String masterKeyAlias,
@NonNull SharedPreferences sharedPreferences,
@NonNull Aead aead,
@NonNull DeterministicAead deterministicAead) {
mFileName = name;
mSharedPreferences = sharedPreferences;
mMasterKeyAlias = masterKeyAlias;
mValueAead = aead;
mKeyDeterministicAead = deterministicAead;
mListeners = new ArrayList<>();
}
/**
* Opens an instance of encrypted SharedPreferences
*
* @param fileName The name of the file to open; can not contain path separators.
* @return The SharedPreferences instance that encrypts all data.
* @throws GeneralSecurityException when a bad master key or keyset has been attempted
* @throws IOException when fileName can not be used
*/
@NonNull
public static SharedPreferences create(@NonNull String fileName,
@NonNull String masterKeyAlias,
@NonNull Context context,
@NonNull PrefKeyEncryptionScheme prefKeyEncryptionScheme,
@NonNull PrefValueEncryptionScheme prefValueEncryptionScheme)
throws GeneralSecurityException, IOException {
TinkConfig.register();
KeysetHandle daeadKeysetHandle = new AndroidKeysetManager.Builder()
.withKeyTemplate(prefKeyEncryptionScheme.getKeyTemplate())
.withSharedPref(context, KEY_KEYSET_ALIAS, fileName)
.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
.build().getKeysetHandle();
KeysetHandle aeadKeysetHandle = new AndroidKeysetManager.Builder()
.withKeyTemplate(prefValueEncryptionScheme.getKeyTemplate())
.withSharedPref(context, VALUE_KEYSET_ALIAS, fileName)
.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
.build().getKeysetHandle();
DeterministicAead daead = daeadKeysetHandle.getPrimitive(DeterministicAead.class);
Aead aead = aeadKeysetHandle.getPrimitive(Aead.class);
return new EncryptedSharedPreferences(fileName, masterKeyAlias,
context.getSharedPreferences(fileName, Context.MODE_PRIVATE), aead, daead);
}
/**
* The encryption scheme to encrypt keys.
*/
public enum PrefKeyEncryptionScheme {
/**
* Pref keys are encrypted deterministically with AES256-SIV-CMAC (RFC 5297).
*
* For more information please see the Tink documentation:
*
* <a href="https://google.github.io/tink/javadoc/tink/1.4.0-rc2/com/google/crypto/tink/daead/AesSivKeyManager.html">AesSivKeyManager</a>.aes256SivTemplate()
*/
AES256_SIV(AesSivKeyManager.aes256SivTemplate());
private final KeyTemplate mDeterministicAeadKeyTemplate;
PrefKeyEncryptionScheme(KeyTemplate keyTemplate) {
mDeterministicAeadKeyTemplate = keyTemplate;
}
KeyTemplate getKeyTemplate() {
return mDeterministicAeadKeyTemplate;
}
}
/**
* The encryption scheme to encrypt values.
*/
public enum PrefValueEncryptionScheme {
/**
* Pref values are encrypted with AES256-GCM. The associated data is the encrypted pref key.
*
* For more information please see the Tink documentation:
*
* <a href="https://google.github.io/tink/javadoc/tink/1.4.0-rc2/com/google/crypto/tink/aead/AesGcmKeyManager.html">AesGcmKeyManager</a>.aes256GcmTemplate()
*/
AES256_GCM(AesGcmKeyManager.aes256GcmTemplate());
private final KeyTemplate mAeadKeyTemplate;
PrefValueEncryptionScheme(KeyTemplate keyTemplates) {
mAeadKeyTemplate = keyTemplates;
}
KeyTemplate getKeyTemplate() {
return mAeadKeyTemplate;
}
}
private static final class Editor implements SharedPreferences.Editor {
private final EncryptedSharedPreferences mEncryptedSharedPreferences;
private final SharedPreferences.Editor mEditor;
private final List<String> mKeysChanged;
private AtomicBoolean mClearRequested = new AtomicBoolean(false);
Editor(EncryptedSharedPreferences encryptedSharedPreferences,
SharedPreferences.Editor editor) {
mEncryptedSharedPreferences = encryptedSharedPreferences;
mEditor = editor;
mKeysChanged = new CopyOnWriteArrayList<>();
}
@Override
@NonNull
public SharedPreferences.Editor putString(@Nullable String key, @Nullable String value) {
if (value == null) {
value = NULL_VALUE;
}
byte[] stringBytes = value.getBytes(UTF_8);
int stringByteLength = stringBytes.length;
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Integer.BYTES
+ stringByteLength);
buffer.putInt(EncryptedType.STRING.getId());
buffer.putInt(stringByteLength);
buffer.put(stringBytes);
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor putStringSet(@Nullable String key,
@Nullable Set<String> values) {
if (values == null) {
values = new ArraySet<>();
values.add(NULL_VALUE);
}
List<byte[]> byteValues = new ArrayList<>(values.size());
int totalBytes = values.size() * Integer.BYTES;
for (String strValue : values) {
byte[] byteValue = strValue.getBytes(UTF_8);
byteValues.add(byteValue);
totalBytes += byteValue.length;
}
totalBytes += Integer.BYTES;
ByteBuffer buffer = ByteBuffer.allocate(totalBytes);
buffer.putInt(EncryptedType.STRING_SET.getId());
for (byte[] bytes : byteValues) {
buffer.putInt(bytes.length);
buffer.put(bytes);
}
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor putInt(@Nullable String key, int value) {
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Integer.BYTES);
buffer.putInt(EncryptedType.INT.getId());
buffer.putInt(value);
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor putLong(@Nullable String key, long value) {
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Long.BYTES);
buffer.putInt(EncryptedType.LONG.getId());
buffer.putLong(value);
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor putFloat(@Nullable String key, float value) {
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Float.BYTES);
buffer.putInt(EncryptedType.FLOAT.getId());
buffer.putFloat(value);
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor putBoolean(@Nullable String key, boolean value) {
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Byte.BYTES);
buffer.putInt(EncryptedType.BOOLEAN.getId());
buffer.put(value ? (byte) 1 : (byte) 0);
putEncryptedObject(key, buffer.array());
return this;
}
@Override
@NonNull
public SharedPreferences.Editor remove(@Nullable String key) {
if (mEncryptedSharedPreferences.isReservedKey(key)) {
throw new SecurityException(key + " is a reserved key for the encryption keyset.");
}
mEditor.remove(mEncryptedSharedPreferences.encryptKey(key));
mKeysChanged.remove(key);
return this;
}
@Override
@NonNull
public SharedPreferences.Editor clear() {
// Set the flag to clear on commit, this operation happens first on commit.
// Cannot use underlying clear operation, it will remove the keysets and
// break the editor.
mClearRequested.set(true);
return this;
}
@Override
public boolean commit() {
clearKeysIfNeeded();
try {
return mEditor.commit();
} finally {
notifyListeners();
mKeysChanged.clear();
}
}
@Override
public void apply() {
clearKeysIfNeeded();
mEditor.apply();
notifyListeners();
}
private void clearKeysIfNeeded() {
// Call "clear" first as per the documentation, remove all keys that haven't
// been modified in this editor.
if (mClearRequested.getAndSet(false)) {
for (String key : mEncryptedSharedPreferences.getAll().keySet()) {
if (!mKeysChanged.contains(key)
&& !mEncryptedSharedPreferences.isReservedKey(key)) {
mEditor.remove(mEncryptedSharedPreferences.encryptKey(key));
}
}
}
}
private void putEncryptedObject(String key, byte[] value) {
if (mEncryptedSharedPreferences.isReservedKey(key)) {
throw new SecurityException(key + " is a reserved key for the encryption keyset.");
}
mKeysChanged.add(key);
if (key == null) {
key = NULL_VALUE;
}
try {
Pair<String, String> encryptedPair = mEncryptedSharedPreferences
.encryptKeyValuePair(key, value);
mEditor.putString(encryptedPair.first, encryptedPair.second);
} catch (GeneralSecurityException ex) {
throw new SecurityException("Could not encrypt data: " + ex.getMessage(), ex);
}
}
private void notifyListeners() {
for (OnSharedPreferenceChangeListener listener :
mEncryptedSharedPreferences.mListeners) {
for (String key : mKeysChanged) {
listener.onSharedPreferenceChanged(mEncryptedSharedPreferences, key);
}
}
}
}
// SharedPreferences methods
@Override
@NonNull
public Map<String, ?> getAll() {
Map<String, ? super Object> allEntries = new HashMap<>();
for (Map.Entry<String, ?> entry : mSharedPreferences.getAll().entrySet()) {
if (!isReservedKey(entry.getKey())) {
String decryptedKey = decryptKey(entry.getKey());
allEntries.put(decryptedKey,
getDecryptedObject(decryptedKey));
}
}
return allEntries;
}
@Nullable
@Override
public String getString(@Nullable String key, @Nullable String defValue) {
Object value = getDecryptedObject(key);
return (value != null && value instanceof String ? (String) value : defValue);
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public Set<String> getStringSet(@Nullable String key, @Nullable Set<String> defValues) {
Set<String> returnValues;
Object value = getDecryptedObject(key);
if (value instanceof Set) {
returnValues = (Set<String>) value;
} else {
returnValues = new ArraySet<>();
}
return returnValues.size() > 0 ? returnValues : defValues;
}
@Override
public int getInt(@Nullable String key, int defValue) {
Object value = getDecryptedObject(key);
return (value != null && value instanceof Integer ? (Integer) value : defValue);
}
@Override
public long getLong(@Nullable String key, long defValue) {
Object value = getDecryptedObject(key);
return (value != null && value instanceof Long ? (Long) value : defValue);
}
@Override
public float getFloat(@Nullable String key, float defValue) {
Object value = getDecryptedObject(key);
return (value != null && value instanceof Float ? (Float) value : defValue);
}
@Override
public boolean getBoolean(@Nullable String key, boolean defValue) {
Object value = getDecryptedObject(key);
return (value != null && value instanceof Boolean ? (Boolean) value : defValue);
}
@Override
public boolean contains(@Nullable String key) {
if (isReservedKey(key)) {
throw new SecurityException(key + " is a reserved key for the encryption keyset.");
}
String encryptedKey = encryptKey(key);
return mSharedPreferences.contains(encryptedKey);
}
@Override
@NonNull
public SharedPreferences.Editor edit() {
return new Editor(this, mSharedPreferences.edit());
}
@Override
public void registerOnSharedPreferenceChangeListener(
@NonNull OnSharedPreferenceChangeListener listener) {
mListeners.add(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
@NonNull OnSharedPreferenceChangeListener listener) {
mListeners.remove(listener);
}
/**
* Internal enum to set the type of encrypted data.
*/
private enum EncryptedType {
STRING(0),
STRING_SET(1),
INT(2),
LONG(3),
FLOAT(4),
BOOLEAN(5);
private final int mId;
EncryptedType(int id) {
mId = id;
}
public int getId() {
return mId;
}
public static EncryptedType fromId(int id) {
switch (id) {
case 0:
return STRING;
case 1:
return STRING_SET;
case 2:
return INT;
case 3:
return LONG;
case 4:
return FLOAT;
case 5:
return BOOLEAN;
}
return null;
}
}
private Object getDecryptedObject(String key) {
if (isReservedKey(key)) {
throw new SecurityException(key + " is a reserved key for the encryption keyset.");
}
if (key == null) {
key = NULL_VALUE;
}
Object returnValue = null;
try {
String encryptedKey = encryptKey(key);
String encryptedValue = mSharedPreferences.getString(encryptedKey, null);
if (encryptedValue != null) {
byte[] cipherText = Base64.decode(encryptedValue, Base64.DEFAULT);
byte[] value = mValueAead.decrypt(cipherText, encryptedKey.getBytes(UTF_8));
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.position(0);
int typeId = buffer.getInt();
EncryptedType type = EncryptedType.fromId(typeId);
switch (type) {
case STRING:
int stringLength = buffer.getInt();
ByteBuffer stringSlice = buffer.slice();
buffer.limit(stringLength);
String stringValue = UTF_8.decode(stringSlice).toString();
if (stringValue.equals(NULL_VALUE)) {
returnValue = null;
} else {
returnValue = stringValue;
}
break;
case INT:
returnValue = buffer.getInt();
break;
case LONG:
returnValue = buffer.getLong();
break;
case FLOAT:
returnValue = buffer.getFloat();
break;
case BOOLEAN:
returnValue = buffer.get() != (byte) 0;
break;
case STRING_SET:
ArraySet<String> stringSet = new ArraySet<>();
while (buffer.hasRemaining()) {
int subStringLength = buffer.getInt();
ByteBuffer subStringSlice = buffer.slice();
subStringSlice.limit(subStringLength);
buffer.position(buffer.position() + subStringLength);
stringSet.add(UTF_8.decode(subStringSlice).toString());
}
if (stringSet.size() == 1 && NULL_VALUE.equals(stringSet.valueAt(0))) {
returnValue = null;
} else {
returnValue = stringSet;
}
break;
}
}
} catch (GeneralSecurityException ex) {
throw new SecurityException("Could not decrypt value. " + ex.getMessage(), ex);
}
return returnValue;
}
String encryptKey(String key) {
if (key == null) {
key = NULL_VALUE;
}
try {
byte[] encryptedKeyBytes = mKeyDeterministicAead.encryptDeterministically(
key.getBytes(UTF_8),
mFileName.getBytes());
return Base64.encode(encryptedKeyBytes);
} catch (GeneralSecurityException ex) {
throw new SecurityException("Could not encrypt key. " + ex.getMessage(), ex);
}
}
String decryptKey(String encryptedKey) {
try {
byte[] clearText = mKeyDeterministicAead.decryptDeterministically(
Base64.decode(encryptedKey, Base64.DEFAULT),
mFileName.getBytes());
String key = new String(clearText, UTF_8);
if (key.equals(NULL_VALUE)) {
key = null;
}
return key;
} catch (GeneralSecurityException ex) {
throw new SecurityException("Could not decrypt key. " + ex.getMessage(), ex);
}
}
/**
* Check usage of the key and value keysets.
*
* @param key the plain text key
*/
boolean isReservedKey(String key) {
if (KEY_KEYSET_ALIAS.equals(key) || VALUE_KEYSET_ALIAS.equals(key)) {
return true;
}
return false;
}
Pair<String, String> encryptKeyValuePair(String key, byte[] value)
throws GeneralSecurityException {
String encryptedKey = encryptKey(key);
byte[] cipherText = mValueAead.encrypt(value, encryptedKey.getBytes(UTF_8));
return new Pair<>(encryptedKey, Base64.encode(cipherText));
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 for the specific language governing permissions and
* limitations under the License.
*/
package au.gov.health.covidsafe.security.crypto;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import javax.crypto.KeyGenerator;
/**
* Convenient methods to create and obtain master keys in Android Keystore.
*
* <p>The master keys are used to encrypt data encryption keys for encrypting files and preferences.
*/
public final class MasterKeys {
private MasterKeys() {
}
private static final int KEY_SIZE = 256;
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
static final String KEYSTORE_PATH_URI = "android-keystore://";
static final String MASTER_KEY_ALIAS = "_androidx_security_master_key_";
@NonNull
public static final KeyGenParameterSpec AES256_GCM_SPEC =
createAES256GCMKeyGenParameterSpec(MASTER_KEY_ALIAS);
/**
* Provides a safe and easy to use KenGenParameterSpec with the settings.
* Algorithm: AES
* Block Mode: GCM
* Padding: No Padding
* Key Size: 256
*
* @param keyAlias The alias for the master key
* @return The spec for the master key with the specified keyAlias
*/
@NonNull
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(
@NonNull String keyAlias) {
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(KEY_SIZE);
return builder.build();
}
/**
* Creates or gets the master key provided
*
* The encryption scheme is required fields to ensure that the type of
* encryption used is clear to developers.
*
* @param keyGenParameterSpec The key encryption scheme
* @return The key alias for the master key
*/
@NonNull
public static String getOrCreate(
@NonNull KeyGenParameterSpec keyGenParameterSpec)
throws GeneralSecurityException, IOException {
validate(keyGenParameterSpec);
if (!MasterKeys.keyExists(keyGenParameterSpec.getKeystoreAlias())) {
generateKey(keyGenParameterSpec);
}
return keyGenParameterSpec.getKeystoreAlias();
}
@VisibleForTesting
static void validate(KeyGenParameterSpec spec) {
if (spec.getKeySize() != KEY_SIZE) {
throw new IllegalArgumentException(
"invalid key size, want " + KEY_SIZE + " bits got " + spec.getKeySize()
+ " bits");
}
if (!Arrays.equals(spec.getBlockModes(), new String[]{KeyProperties.BLOCK_MODE_GCM})) {
throw new IllegalArgumentException(
"invalid block mode, want " + KeyProperties.BLOCK_MODE_GCM + " got "
+ Arrays.toString(spec.getBlockModes()));
}
if (spec.getPurposes() != (KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)) {
throw new IllegalArgumentException(
"invalid purposes mode, want PURPOSE_ENCRYPT | PURPOSE_DECRYPT got "
+ spec.getPurposes());
}
if (!Arrays.equals(spec.getEncryptionPaddings(), new String[]
{KeyProperties.ENCRYPTION_PADDING_NONE})) {
throw new IllegalArgumentException(
"invalid padding mode, want " + KeyProperties.ENCRYPTION_PADDING_NONE + " got "
+ Arrays.toString(spec.getEncryptionPaddings()));
}
if (spec.isUserAuthenticationRequired()
&& spec.getUserAuthenticationValidityDurationSeconds() < 1) {
throw new IllegalArgumentException(
"per-operation authentication is not supported "
+ "(UserAuthenticationValidityDurationSeconds must be >0)");
}
}
private static void generateKey(@NonNull KeyGenParameterSpec keyGenParameterSpec)
throws GeneralSecurityException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE);
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
private static boolean keyExists(@NonNull String keyAlias)
throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
return keyStore.containsAlias(keyAlias);
}
}

View file

@ -221,7 +221,7 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
} }
private fun allPermissionsEnabled(): Boolean { private fun allPermissionsEnabled(): Boolean {
val bluetoothEnabled = isBlueToothEnabled() ?: true val bluetoothEnabled = isBlueToothEnabled() ?: false
val pushNotificationEnabled = isPushNotificationEnabled() ?: true val pushNotificationEnabled = isPushNotificationEnabled() ?: true
val nonBatteryOptimizationAllowed = isNonBatteryOptimizationAllowed() ?: true val nonBatteryOptimizationAllowed = isNonBatteryOptimizationAllowed() ?: true
val locationStatusAllowed = isFineLocationEnabled() ?: true val locationStatusAllowed = isFineLocationEnabled() ?: true

View file

@ -21,20 +21,15 @@ class RegistrationConsentFragment : PagerChildFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
registration_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
updateButtonState()
}
// set accessibility focus to the title "I consent to the Australian ..." // set accessibility focus to the title "I consent to the Australian ..."
registration_consent_text.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) registration_consent_text.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
updateButtonState()
} }
override fun updateButtonState() { override fun updateButtonState() {
if (registration_consent_checkbox.isChecked) {
(activity as? PagerContainer)?.enableNextButton() (activity as? PagerContainer)?.enableNextButton()
} else {
(activity as? PagerContainer)?.disableNextButton()
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -42,7 +37,7 @@ class RegistrationConsentFragment : PagerChildFragment() {
root.removeAllViews() root.removeAllViews()
} }
override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.registration_consent_button) { override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.consent_button) {
navigateTo(RegistrationConsentFragmentDirections.actionRegistrationConsentFragmentToPersonalDetailsFragment().actionId) navigateTo(RegistrationConsentFragmentDirections.actionRegistrationConsentFragmentToPersonalDetailsFragment().actionId)
} }
} }

View file

@ -24,17 +24,10 @@ class UnderSixteenFragment : PagerChildFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
under_sixteen_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
updateButtonState() updateButtonState()
} }
}
override fun onPause() { override fun getUploadButtonLayout(): UploadButtonLayout = UploadButtonLayout.ContinueLayout(R.string.consent_button) {
super.onPause()
under_sixteen_checkbox.setOnCheckedChangeListener(null)
}
override fun getUploadButtonLayout(): UploadButtonLayout = UploadButtonLayout.ContinueLayout(R.string.under_sixteen_button) {
val bundle = bundleOf( val bundle = bundleOf(
EnterNumberFragment.ENTER_NUMBER_DESTINATION_ID to R.id.action_otpFragment_to_permissionFragment, EnterNumberFragment.ENTER_NUMBER_DESTINATION_ID to R.id.action_otpFragment_to_permissionFragment,
EnterNumberFragment.ENTER_NUMBER_PROGRESS to 2) EnterNumberFragment.ENTER_NUMBER_PROGRESS to 2)
@ -42,10 +35,6 @@ class UnderSixteenFragment : PagerChildFragment() {
} }
override fun updateButtonState() { override fun updateButtonState() {
if (under_sixteen_checkbox.isChecked) {
enableContinueButton() enableContinueButton()
} else {
disableContinueButton()
}
} }
} }

View file

@ -28,26 +28,20 @@ class UploadStepFourFragment : PagerChildFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
updateButtonState() updateButtonState()
}
// set accessibility focus to the title // set accessibility focus to the title
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
} }
override fun updateButtonState() { override fun updateButtonState() {
if (upload_consent_checkbox.isChecked) {
enableContinueButton() enableContinueButton()
} else {
disableContinueButton()
}
} }
override val navigationIcon: Int? = R.drawable.ic_up override val navigationIcon: Int? = R.drawable.ic_up
override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout( override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(
R.string.action_continue) { R.string.consent_button) {
navigateToVerifyUploadPin() navigateToVerifyUploadPin()
} }

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="26.086956"
android:viewportHeight="26.086956"
android:tint="#FFFFFF">
<group android:translateX="1.0434783"
android:translateY="1.0434783">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</group>
</vector>

View file

@ -32,6 +32,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/white" android:background="@color/white"
android:paddingTop="@dimen/keyline_0"
android:paddingBottom="@dimen/keyline_0"
android:minHeight="@dimen/external_link_height" android:minHeight="@dimen/external_link_height"
app:external_linkCard_content="@string/home_set_complete_external_link_share_content" app:external_linkCard_content="@string/home_set_complete_external_link_share_content"
app:external_linkCard_icon="@drawable/ic_home_share" app:external_linkCard_icon="@drawable/ic_home_share"
@ -52,6 +54,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/white" android:background="@color/white"
android:minHeight="@dimen/external_link_height" android:minHeight="@dimen/external_link_height"
android:paddingTop="@dimen/keyline_0"
android:paddingBottom="@dimen/keyline_0"
app:external_linkCard_content="@string/home_set_complete_external_link_been_contacted_content" app:external_linkCard_content="@string/home_set_complete_external_link_been_contacted_content"
app:external_linkCard_icon="@drawable/ic_upload_icon" app:external_linkCard_icon="@drawable/ic_upload_icon"
app:external_linkCard_icon_background="@drawable/background_circular_green" app:external_linkCard_icon_background="@drawable/background_circular_green"
@ -83,6 +87,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/white" android:background="@color/white"
android:minHeight="@dimen/external_link_height" android:minHeight="@dimen/external_link_height"
android:paddingTop="@dimen/keyline_0"
android:paddingBottom="@dimen/keyline_0"
app:external_linkCard_content="@string/home_set_complete_external_link_app_content" app:external_linkCard_content="@string/home_set_complete_external_link_app_content"
app:external_linkCard_icon="@drawable/ic_home_news" app:external_linkCard_icon="@drawable/ic_home_news"
app:external_linkCard_icon_background="@drawable/background_circular_black" app:external_linkCard_icon_background="@drawable/background_circular_black"
@ -130,9 +136,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/white" android:background="@color/white"
android:minHeight="@dimen/external_link_height" android:minHeight="@dimen/external_link_height"
app:external_linkCard_title="@string/home_set_complete_external_link_help_topics_title" android:paddingTop="@dimen/keyline_0"
android:paddingBottom="@dimen/keyline_0"
app:external_linkCard_content="@string/home_set_complete_external_link_help_topics_content" app:external_linkCard_content="@string/home_set_complete_external_link_help_topics_content"
app:external_linkCard_icon="@drawable/ic_question_circle" /> app:external_linkCard_icon="@drawable/ic_question_circle"
app:external_linkCard_title="@string/home_set_complete_external_link_help_topics_title" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</merge> </merge>

View file

@ -59,15 +59,14 @@
app:layout_constraintTop_toBottomOf="@+id/registration_consent_first_paragraph" app:layout_constraintTop_toBottomOf="@+id/registration_consent_first_paragraph"
app:ul_view_text="@string/registration_consent_second_paragraph" /> app:ul_view_text="@string/registration_consent_second_paragraph" />
<com.google.android.material.checkbox.MaterialCheckBox <TextView
android:id="@+id/registration_consent_checkbox" android:id="@+id/registration_consent_call_for_action"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/keyline_8" android:layout_marginTop="@dimen/keyline_4"
android:text="@string/registration_consent_checkbox" android:text="@string/consent_call_for_action"
android:textSize="18sp" android:textAppearance="?textAppearanceBody1"
android:gravity="center_vertical" android:textColorLink="@color/hyperlink_enabled"
android:layout_marginTop="@dimen/keyline_2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registration_consent_second_paragraph" /> app:layout_constraintTop_toBottomOf="@+id/registration_consent_second_paragraph" />

View file

@ -60,14 +60,13 @@
app:layout_constraintTop_toBottomOf="@+id/under_sixteen_first_paragraph" app:layout_constraintTop_toBottomOf="@+id/under_sixteen_first_paragraph"
app:ul_view_text="@string/under_sixteen_second_paragraph" /> app:ul_view_text="@string/under_sixteen_second_paragraph" />
<com.google.android.material.checkbox.MaterialCheckBox <TextView
android:id="@+id/under_sixteen_checkbox" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/keyline_8" android:layout_marginTop="@dimen/keyline_4"
android:textSize="18sp" android:text="@string/consent_call_for_action"
android:text="@string/under_sixteen_further_checkbox" android:textAppearance="?textAppearanceBody1"
android:layout_marginTop="@dimen/keyline_2" android:textColorLink="@color/hyperlink_enabled"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/under_sixteen_second_paragraph" /> app:layout_constraintTop_toBottomOf="@+id/under_sixteen_second_paragraph" />

View file

@ -32,16 +32,14 @@
android:layout_marginEnd="@dimen/keyline_5" android:layout_marginEnd="@dimen/keyline_5"
android:text="@string/upload_step_4_sub_header"/> android:text="@string/upload_step_4_sub_header"/>
<com.google.android.material.checkbox.MaterialCheckBox <TextView
android:id="@+id/upload_consent_checkbox" style="?attr/textAppearanceBody2"
style="?attr/textAppearanceBody1" android:layout_marginStart="@dimen/keyline_5"
android:layout_marginStart="@dimen/keyline_4"
android:layout_marginTop="@dimen/keyline_4" android:layout_marginTop="@dimen/keyline_4"
android:layout_marginEnd="@dimen/keyline_4" android:layout_marginEnd="@dimen/keyline_5"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/keyline_8" android:text="@string/consent_call_for_action" />
android:text="@string/upload_consent" />
</LinearLayout> </LinearLayout>

View file

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/external_link_height" android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView <ImageView

View file

@ -43,16 +43,18 @@
<!-- OnBoarding Data Privacy --> <!-- OnBoarding Data Privacy -->
<string name="data_privacy_headline">Registration and privacy</string> <string name="data_privacy_headline">Registration and privacy</string>
<string name="data_privacy_headline_content_description">Heading, Registration and privacy</string> <string name="data_privacy_headline_content_description">Heading, Registration and privacy</string>
<string name="data_privacy_content">It is important that you read the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a>.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, <a href="https://www.covidsafe.gov.au/help-topics.html">you may also ask for your information</a> to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with your device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the <a href="https://www.health.gov.au/">Australian Government Department of Health website</a>.\n\nSee the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details about your rights about your information and how it will be handled and shared.</string> <string name="data_privacy_content">It is important that you read the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a>.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, <a href="https://www.covidsafe.gov.au/help-topics.html">you may also ask for your information</a> to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with you on their device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the <a href="https://www.health.gov.au/">Australian Government Department of Health website</a>.\n\nSee the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details about your rights about your information and how it will be handled and shared.</string>
<string name="data_privacy_button">Next</string> <string name="data_privacy_button">Next</string>
<!-- Shared Consent Actions-->
<string name="consent_call_for_action">Select \'I agree\' to confirm consent.</string>
<string name="consent_button">I agree</string>
<!-- Onboarding Registration Consent --> <!-- Onboarding Registration Consent -->
<string name="registration_consent_headline">Registration consent</string> <string name="registration_consent_headline">Registration consent</string>
<string name="registration_consent_content">I consent to the Australian Department of Health collecting:</string> <string name="registration_consent_content">I consent to the Australian Department of Health collecting:</string>
<string name="registration_consent_first_paragraph">My registration information to allow contact tracing by state and territory health officials.</string> <string name="registration_consent_first_paragraph">My registration information to allow contact tracing by state or territory health officials.</string>
<string name="registration_consent_second_paragraph">My contact information from other COVIDSafe users after they test positive for COVID-19.</string> <string name="registration_consent_second_paragraph">My contact information from other COVIDSafe users after they test positive for COVID-19.</string>
<string name="registration_consent_checkbox">I consent.</string>
<string name="registration_consent_button">Continue</string>
<!-- OnBoarding Personal details --> <!-- OnBoarding Personal details -->
<string name="personal_details_headline">Enter your details</string> <string name="personal_details_headline">Enter your details</string>
@ -93,8 +95,6 @@
<string name="under_sixteen_content">I confirm my parent or guardian consents to the Australian Department of Health collecting:</string> <string name="under_sixteen_content">I confirm my parent or guardian consents to the Australian Department of Health collecting:</string>
<string name="under_sixteen_first_paragraph">My registration information to allow contact tracing by state and territory health officials.</string> <string name="under_sixteen_first_paragraph">My registration information to allow contact tracing by state and territory health officials.</string>
<string name="under_sixteen_second_paragraph">My contact information from other COVIDSafe users after they test positive for COVID-19.</string> <string name="under_sixteen_second_paragraph">My contact information from other COVIDSafe users after they test positive for COVID-19.</string>
<string name="under_sixteen_further_checkbox">I confirm.</string>
<string name="under_sixteen_button">Continue</string>
<!-- OnBoarding Enter Number --> <!-- OnBoarding Enter Number -->
@ -125,7 +125,7 @@
<string name="change_device_name_headline">Your device name</string> <string name="change_device_name_headline">Your device name</string>
<string name="change_device_name_headline_content_description">Heading, Your device name</string> <string name="change_device_name_headline_content_description">Heading, Your device name</string>
<string name="change_device_name_content_line_1">The current name of your device is %s.</string> <string name="change_device_name_content_line_1">The current name of your device is %s.</string>
<string name="change_device_name_content_line_2">Other Bluetooth® devices around you will be able to see this name. You may like to consider making the device name anonymous.</string> <string name="change_device_name_content_line_2">Other Bluetooth® devices around you will be able to see this name. We recommend to use a device name that does not include your personal details.</string>
<string name="change_device_name_new_device_name">New device name</string> <string name="change_device_name_new_device_name">New device name</string>
<string name="change_device_name_default_device_name">Android phone</string> <string name="change_device_name_default_device_name">Android phone</string>
<string name="change_device_name_primary_action">Change and continue</string> <string name="change_device_name_primary_action">Change and continue</string>
@ -200,7 +200,6 @@
<string name="upload_step_4_header">Upload consent</string> <string name="upload_step_4_header">Upload consent</string>
<string name="upload_step_4_header_content_description">Heading, Upload consent</string> <string name="upload_step_4_header_content_description">Heading, Upload consent</string>
<string name="upload_step_4_sub_header">Unless you consent, your close contact information will not be uploaded.\n\nIf you consent, your close contact information will be uploaded and shared with state or territory health officials for contact tracing purposes.\n\nRead the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details.</string> <string name="upload_step_4_sub_header">Unless you consent, your close contact information will not be uploaded.\n\nIf you consent, your close contact information will be uploaded and shared with state or territory health officials for contact tracing purposes.\n\nRead the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details.</string>
<string name='upload_consent'>I consent to upload my information</string>
<string name="upload_step_verify_pin_header">Upload your information</string> <string name="upload_step_verify_pin_header">Upload your information</string>
<string name="upload_step_verify_pin_header_content_description">Heading, Upload your information</string> <string name="upload_step_verify_pin_header_content_description">Heading, Upload your information</string>
<string name="upload_step_verify_pin_sub_header">A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information.</string> <string name="upload_step_verify_pin_sub_header">A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information.</string>