├── .gitignore ├── README.MD ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── redmadrobot │ │ └── advancedtink │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── redmadrobot │ │ │ └── advancedtink │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── encryption │ │ │ ├── CryptoUtils.kt │ │ │ ├── SysUtils.kt │ │ │ ├── TinkKeyEncryption.kt │ │ │ └── TinkValueEncryption.kt │ │ │ └── extension │ │ │ └── PreferencesExt.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── redmadrobot │ └── advancedtink │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── scheme-1.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | bin/ 3 | gen/ 4 | out/ 5 | 6 | # Gradle files 7 | .gradle/ 8 | build/ 9 | 10 | # Local configuration file (sdk path, etc) 11 | local.properties 12 | 13 | # Log Files 14 | *.log 15 | 16 | # Android Studio Navigation editor temp files 17 | .navigation/ 18 | 19 | # Android Studio captures folder 20 | captures/ 21 | 22 | # IntelliJ 23 | *.iml 24 | *.ipr 25 | *.iws 26 | .idea 27 | 28 | # Keystore files 29 | # Uncomment the following line if you do not want to check your keystore files in. 30 | *.jks 31 | 32 | # External native build folder generated in Android Studio 2.2 and later 33 | .externalNativeBuild 34 | 35 | # Google Services (e.g. APIs or Firebase) 36 | google-services.json 37 | 38 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Advanced Tink 2 | 3 | Пример использования библиотеки [Tink](https://github.com/google/tink) вместе с библиотекой [BinaryPreferences](https://github.com/yandextaxitech/binaryprefs). Подключить ее можно следующим образом: 4 | 5 | `implementation 'com.github.yandextaxitech:binaryprefs:1.0.0'` 6 | 7 | ### Зачем? 8 | Комбинация этих двух библиотек, позволяет достаточно просто реализовать безопасное хранение пользовательских данных, даже на рутованных устройствах. Это возможно, потому что Tink хранит ключи в AndroidKeystore. С другой стороны, библиотека BinaryPreferences поддерживает бесшовное шифрование всех сохраняемых данных, так что вам даже не придется об этом задумываться. 9 | 10 | ### Как это работает 11 | ![alt](images/scheme-1.png) 12 | 13 | Чтобы объединить 2 библиотеки, были написаны альтернативные реализации шифрования с использованием Tink. Найти их можно в классах `TinkKeyEncryption` и `TinkValueEncryption`. Применение двух сразу не является обязательным и задается в настройках BinaryPreferences. 14 | 15 | ```kotlin 16 | val preferences by lazy { 17 | BinaryPreferencesBuilder(this) 18 | .keyEncryption(TinkKeyEncryption(this, daead)) 19 | .valueEncryption(TinkValueEncryption(this, aead)) 20 | .build() 21 | } 22 | ``` 23 | 24 | **Важный момент**: При AEAD шифровании, Tink каждый раз генерирует новый вектор инициализации, что на выходе дает **разный** шифротекст при каждом повторном шифровании. По этой причине, для шифрования ключей преференсов применяется т.н. "детерминированный AEAD", который всегда выдает стабильный шифротекст. Это является менее безопасным *в общем случае*, но вполне подходит для данной задачи. 25 | 26 | ### Для дальнейшего изучения 27 | 28 | * [Работа с Tink из Java](https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md) 29 | * [Описание всех доступных крипто-примитивов](https://github.com/google/tink/blob/master/docs/PRIMITIVES.md) 30 | * [Что такое AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption) 31 | 32 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.redmadrobot.advancedtink" 11 | minSdkVersion 23 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 28 | implementation 'com.android.support:appcompat-v7:28.0.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 30 | 31 | implementation 'com.google.crypto.tink:tink-android:1.2.2' 32 | implementation 'com.github.yandextaxitech:binaryprefs:1.0.1' 33 | 34 | 35 | testImplementation 'junit:junit:4.12' 36 | 37 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 38 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 39 | } 40 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/redmadrobot/advancedtink/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getTargetContext() 20 | assertEquals("com.redmadrobot.advancedtink", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/App.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink 2 | 3 | import android.app.Application 4 | import com.google.crypto.tink.aead.AeadFactory 5 | import com.google.crypto.tink.aead.AeadKeyTemplates 6 | import com.google.crypto.tink.config.TinkConfig 7 | import com.google.crypto.tink.daead.DeterministicAeadFactory 8 | import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates 9 | import com.google.crypto.tink.integration.android.AndroidKeysetManager 10 | import com.ironz.binaryprefs.BinaryPreferencesBuilder 11 | import com.redmadrobot.advancedtink.encryption.TinkKeyEncryption 12 | import com.redmadrobot.advancedtink.encryption.TinkValueEncryption 13 | import java.security.GeneralSecurityException 14 | 15 | 16 | class App : Application() { 17 | companion object { 18 | private const val KEYSET_NAME = "master_keyset" 19 | private const val PREFERENCE_FILE = "master_key_preference" 20 | private const val MASTER_KEY_URI = "android-keystore://master_key" 21 | 22 | private const val DKEYSET_NAME = "dmaster_keyset" 23 | private const val DPREFERENCE_FILE = "dmaster_key_preference" 24 | private const val DMASTER_KEY_URI = "android-keystore://dmaster_key" 25 | } 26 | 27 | //region Use DI, please... 28 | 29 | val aead by lazy { 30 | val keysetHandle = AndroidKeysetManager.Builder() 31 | .withSharedPref(this, KEYSET_NAME, PREFERENCE_FILE) 32 | .withKeyTemplate(AeadKeyTemplates.AES256_GCM) 33 | .withMasterKeyUri(MASTER_KEY_URI) 34 | .build() 35 | .keysetHandle 36 | 37 | AeadFactory.getPrimitive(keysetHandle) 38 | } 39 | 40 | val daead by lazy { 41 | val keysetHandle = AndroidKeysetManager.Builder() 42 | .withSharedPref(this, DKEYSET_NAME, DPREFERENCE_FILE) 43 | .withKeyTemplate(DeterministicAeadKeyTemplates.AES256_SIV) 44 | .withMasterKeyUri(DMASTER_KEY_URI) 45 | .build() 46 | .keysetHandle 47 | 48 | DeterministicAeadFactory.getPrimitive(keysetHandle) 49 | } 50 | 51 | val preferences by lazy { 52 | BinaryPreferencesBuilder(this) 53 | .keyEncryption(TinkKeyEncryption(this, daead)) 54 | .valueEncryption(TinkValueEncryption(this, aead)) 55 | .build() 56 | } 57 | 58 | //endregion 59 | 60 | override fun onCreate() { 61 | super.onCreate() 62 | 63 | initTink() 64 | } 65 | 66 | private fun initTink() { 67 | try { 68 | TinkConfig.register() 69 | } catch (e: GeneralSecurityException) { 70 | throw RuntimeException(e) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import com.redmadrobot.advancedtink.encryption.CryptoUtils 6 | import com.redmadrobot.advancedtink.extension.modify 7 | import kotlinx.android.synthetic.main.activity_main.* 8 | 9 | class MainActivity : AppCompatActivity() { 10 | companion object { 11 | private const val USERNAME_KEY = "USERNAME_KEY" 12 | } 13 | 14 | private val app by lazy { application as App } 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_main) 19 | 20 | initCryptoScenarioViews() 21 | initPreferenesScenario() 22 | } 23 | 24 | private fun initCryptoScenarioViews() { 25 | button_encrypt.setOnClickListener { 26 | val plainText = "${edit_text_plain.text}" 27 | val cipherText = app.aead.encrypt(plainText.toByteArray(), null) 28 | 29 | edit_text_cipher.setText(CryptoUtils.base64Encode(cipherText)) 30 | } 31 | 32 | button_decrypt.setOnClickListener { 33 | val cipherText = CryptoUtils.base64Decode("${edit_text_cipher.text}") 34 | val plainText = app.aead.decrypt(cipherText, null) 35 | 36 | edit_text_plain.setText(String(plainText)) 37 | } 38 | } 39 | 40 | private fun initPreferenesScenario() { 41 | button_save.setOnClickListener { 42 | app.preferences.modify { 43 | putString(USERNAME_KEY, "${edit_text_username.text}") 44 | } 45 | } 46 | 47 | button_load.setOnClickListener { 48 | val username = app.preferences.getString(USERNAME_KEY, "no username") 49 | edit_text_username.setText(username) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/encryption/CryptoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink.encryption 2 | 3 | import android.util.Base64 4 | import java.security.MessageDigest 5 | import java.security.NoSuchAlgorithmException 6 | 7 | 8 | object CryptoUtils { 9 | fun sha256(text: String, encodedBase64: Boolean = false): String { 10 | val hashBytes = sha256(text) 11 | 12 | return if (encodedBase64) { 13 | Base64.encodeToString(hashBytes, Base64.NO_WRAP) 14 | } else { 15 | hashBytes.map { String.format("%02x", it) }.reduce { acc, s -> acc + s } 16 | } 17 | } 18 | 19 | fun sha256(text: String) = sha256(text.toByteArray()) 20 | 21 | fun sha256(byteArray: ByteArray): ByteArray { 22 | val digest = try { 23 | MessageDigest.getInstance("SHA-256") 24 | } catch (e: NoSuchAlgorithmException) { 25 | MessageDigest.getInstance("SHA") 26 | } 27 | 28 | return with(digest) { 29 | update(byteArray) 30 | digest() 31 | } 32 | } 33 | 34 | fun base64Encode(bytes: ByteArray): String { 35 | return Base64.encodeToString(bytes, Base64.DEFAULT) 36 | } 37 | 38 | fun base64Decode(string: String): ByteArray { 39 | return Base64.decode(string, Base64.DEFAULT) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/encryption/SysUtils.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink.encryption 2 | 3 | import android.annotation.SuppressLint 4 | import android.annotation.TargetApi 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | 9 | 10 | object SysUtils { 11 | @Suppress("DEPRECATION") 12 | @TargetApi(Build.VERSION_CODES.P) 13 | @SuppressLint("WrongConstant", "PackageManagerGetSignatures") 14 | fun getSignatureSha(context: Context): ByteArray { 15 | val signatures = with(context.packageManager) { 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 17 | getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES) 18 | .signingInfo 19 | .apkContentsSigners 20 | } else { 21 | getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures 22 | } 23 | } 24 | 25 | 26 | return CryptoUtils.sha256(signatures.first().toByteArray()) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/encryption/TinkKeyEncryption.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink.encryption 2 | 3 | import android.content.Context 4 | import android.util.Base64 5 | import com.google.crypto.tink.DeterministicAead 6 | import com.ironz.binaryprefs.encryption.KeyEncryption 7 | 8 | 9 | class TinkKeyEncryption(context: Context, private val aead: DeterministicAead) : KeyEncryption { 10 | private val signature = SysUtils.getSignatureSha(context) 11 | private val encoderFlags = Base64.NO_WRAP or Base64.URL_SAFE 12 | 13 | override fun encrypt(plaintext: String): String { 14 | val ciphertext = aead.encryptDeterministically(plaintext.toByteArray(), signature) 15 | return Base64.encodeToString(ciphertext, encoderFlags) 16 | } 17 | 18 | override fun decrypt(cipher: String): String { 19 | val cipertext = Base64.decode(cipher, encoderFlags) 20 | return String(aead.decryptDeterministically(cipertext, signature)) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/encryption/TinkValueEncryption.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink.encryption 2 | 3 | import android.content.Context 4 | import android.util.Base64 5 | import com.google.crypto.tink.Aead 6 | import com.ironz.binaryprefs.encryption.ValueEncryption 7 | 8 | 9 | class TinkValueEncryption(context: Context, private val aead: Aead) : ValueEncryption { 10 | private val signature = SysUtils.getSignatureSha(context).takeLast(16).toByteArray() 11 | 12 | override fun encrypt(plaintext: ByteArray): ByteArray { 13 | val ciphertext = aead.encrypt(plaintext, signature) 14 | return Base64.encode(ciphertext, Base64.DEFAULT) 15 | } 16 | 17 | override fun decrypt(cipher: ByteArray): ByteArray { 18 | val cipertext = Base64.decode(cipher, Base64.DEFAULT) 19 | return aead.decrypt(cipertext, signature) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/advancedtink/extension/PreferencesExt.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.advancedtink.extension 2 | 3 | import com.ironz.binaryprefs.Preferences 4 | import com.ironz.binaryprefs.PreferencesEditor 5 | 6 | 7 | inline fun Preferences.modify(commit: Boolean = false, action: PreferencesEditor.() -> Unit) { 8 | val editor = edit() 9 | 10 | action(editor) 11 | 12 | if (commit) { 13 | editor.commit() 14 | } else { 15 | editor.apply() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 29 | 30 |