├── .github └── workflows │ └── blank.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE.md ├── SamplePreferenceRepository ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── tech │ └── thdev │ └── samplepreference │ └── repository │ ├── NonSecurePreferences.kt │ └── SecurePreferences.kt ├── _config.yml ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── tech │ │ └── thdev │ │ └── encrypteddatastorepreference │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── tech │ │ │ └── thdev │ │ │ └── encrypteddatastorepreference │ │ │ ├── CompositionLocalProvider.kt │ │ │ ├── LocalMainViewModel.kt │ │ │ ├── SecureSampleActivity.kt │ │ │ ├── SecureSampleViewModel.kt │ │ │ ├── compose │ │ │ ├── SecureSampleScreen.kt │ │ │ └── component │ │ │ │ └── TestComponent.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── tech │ └── thdev │ └── encrypteddatastorepreference │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── tech │ └── thdev │ └── gradle │ ├── dependencies │ └── Publish.kt │ └── locals │ └── base │ ├── lib-publish-android.gradle.kts │ └── lib-publish.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── useful-encrypted-data-store-preferences-ksp-annotations ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── tech │ └── thdev │ └── useful │ └── encrypted │ └── data │ └── store │ └── preferences │ └── ksp │ └── annotations │ ├── UsefulPreferences.kt │ └── value │ ├── ClearValues.kt │ ├── GetValue.kt │ └── SetValue.kt ├── useful-encrypted-data-store-preferences-ksp ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── tech │ │ │ └── thdev │ │ │ └── useful │ │ │ └── encrypted │ │ │ └── data │ │ │ └── store │ │ │ └── preferences │ │ │ └── ksp │ │ │ ├── EncryptedDataStorePreferencesProcessor.kt │ │ │ ├── EncryptedDataStorePreferencesProcessorProvider.kt │ │ │ └── internal │ │ │ ├── DataStoreConst.kt │ │ │ ├── StringUtil.kt │ │ │ ├── Util.kt │ │ │ ├── generate │ │ │ ├── GeneratePreferences.kt │ │ │ └── function │ │ │ │ ├── GenerateClearFunction.kt │ │ │ │ ├── GenerateGetFunction.kt │ │ │ │ └── GenerateSetFunction.kt │ │ │ ├── model │ │ │ ├── DataType.kt │ │ │ └── ResearchModel.kt │ │ │ └── visitor │ │ │ └── FindDataStorePreferences.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider │ └── test │ └── java │ └── tech │ └── thdev │ └── useful │ └── encrypted │ └── data │ └── store │ └── preferences │ └── ksp │ └── EncryptedDataStorePreferencesProcessorProviderTest.kt └── useful-encrypted-data-store-preferences-security ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── main ├── AndroidManifest.xml └── java │ └── tech │ └── thdev │ └── useful │ └── encrypted │ └── data │ └── store │ └── preferences │ ├── extensions │ └── UsefulSecurityDataStoreExtensions.kt │ └── security │ ├── CipherWrapper.kt │ ├── InternalSecureException.kt │ ├── UsefulSecurity.kt │ ├── UsefulSecurityImpl.kt │ └── util │ └── UsefulSecurityUtil.kt └── test └── java └── tech └── thdev └── useful └── encrypted └── data └── store └── preferences ├── extensions ├── MockDataStore.kt └── UsefulSecurityDataStoreExtensionsKtTest.kt └── security └── UsefulSecurityImplTest.kt /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Android Pull Request CI 4 | 5 | on: 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout the code 15 | uses: actions/checkout@v3 16 | 17 | - name: set up JDK 17 18 | uses: actions/setup-java@v3 19 | with: 20 | distribution: adopt 21 | java-version: 17 22 | 23 | - name: set up Android SDK 24 | uses: android-actions/setup-android@v2 25 | 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | 29 | - name: Clean the project 30 | run: ./gradlew clean 31 | 32 | - name: Test the project 33 | run: ./gradlew test --no-build-cache 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 thdev.tech 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | Android Encrypted DataStorePreference. 4 | 5 | Include Data Encrypt in Android DataStorePreference. This library use KSP(ksp version 1.9.25-1.0.20). 6 | 7 | ## Download 8 | 9 | lastVersion 2.0.21-1.0.28-1.2.0 10 | 11 | ```kotlin 12 | plugins { 13 | id("com.google.devtools.ksp") 14 | } 15 | 16 | dependencies { 17 | ksp("tech.thdev:useful-encrypted-data-store-preferences-ksp:$lastVersion") 18 | implementation("tech.thdev:useful-encrypted-data-store-preferences-ksp-annotations:$lastVersion") 19 | implementation("tech.thdev:useful-encrypted-data-store-preferences-security:$lastVersion") 20 | } 21 | ``` 22 | 23 | Release version are available in [Sonatyp's repository.](https://search.maven.org/search?q=tech.thdev) 24 | 25 | ## Use Code 26 | 27 | Use code - 1.2.0 28 | 29 | default value option. and only string. 30 | 31 | ```text 32 | Int -> "0" 33 | Double -> "0.0" 34 | String -> "message" 35 | Boolean -> "true" or "false" 36 | Float -> "0.0" 37 | Long -> "0" 38 | ``` 39 | 40 | ```kotlin 41 | @UsefulPreferences(/* option. Not use security - disableSecure = true */) 42 | interface SecurityPreferences { 43 | 44 | @GetValue(KEY_INT, /* option : defaultValue = "123" */) 45 | fun flowInt(): Flow 46 | 47 | @GetValue(KEY_INT, /* option : defaultValue = "123" */) 48 | suspend fun getInt(): Int 49 | 50 | @SetValue(KEY_INT) 51 | suspend fun flowInt(value: Int) 52 | 53 | @GetValue(KEY_DOUBLE, /* option : defaultValue = "123.0" */) 54 | fun flowDouble(): Flow 55 | 56 | @SetValue(KEY_DOUBLE) 57 | suspend fun setDouble(value: Double) 58 | 59 | @GetValue(KEY_STRING, /* option : defaultValue = "message" */) 60 | fun flowString(): Flow 61 | 62 | @SetValue(KEY_STRING) 63 | suspend fun setString(value: String) 64 | 65 | @GetValue(KEY_BOOLEAN, /* option : defaultValue = "true/false" */) 66 | fun flowBoolean(): Flow 67 | 68 | @SetValue(KEY_BOOLEAN) 69 | suspend fun setBoolean(value: Boolean) 70 | 71 | @GetValue(KEY_FLOAT, /* option : defaultValue = "123.0" */) 72 | fun flowFloat(): Flow 73 | 74 | @SetValue(KEY_FLOAT) 75 | suspend fun setFloat(value: Float) 76 | 77 | @GetValue(KEY_LONG, /* option : defaultValue = "123" */) 78 | fun flowLong(): Flow 79 | 80 | @SetValue(KEY_LONG) 81 | suspend fun setLong(value: Long) 82 | 83 | @ClearValues 84 | suspend fun clearAll() 85 | 86 | companion object { 87 | 88 | private const val KEY_INT = "key-int" 89 | private const val KEY_DOUBLE = "key-double" 90 | private const val KEY_STRING = "key-string" 91 | private const val KEY_BOOLEAN = "key-boolean" 92 | private const val KEY_FLOAT = "key-float" 93 | private const val KEY_LONG = "key-long" 94 | } 95 | } 96 | ``` 97 | 98 | and rebuild. 99 | 100 | use Activity or application 101 | 102 | Use the provided security or implement UsefulSecurity inheritance. 103 | 104 | ```kotlin 105 | class SampleActivity { 106 | private val Context.dataStore by preferencesDataStore(name = "security-preference") 107 | 108 | private val securityPreference: SecurityPreferences by lazy { 109 | dataStore.generateSecurePreferences( 110 | generateUsefulSecurity( 111 | keyStoreAlias = packageName, 112 | ), 113 | ) 114 | } 115 | 116 | // SetValue 117 | coroutineScope.launch { 118 | securityPreference.setInt(++count) 119 | } 120 | 121 | // GetValue -- return Flow 122 | securityPreference.flowInt() 123 | .onEach { 124 | Toast.makeText(this@MainActivity, "Current Int $it", Toast.LENGTH_SHORT).show() 125 | } 126 | .flowOn(Dispatchers.MAIN) 127 | .catch { 128 | it.printStackTrace() 129 | } 130 | .launchIn(this) 131 | 132 | // GetValue -- suspend function 133 | coroutineScope.launch { 134 | securityPreference.getInt() 135 | } 136 | } 137 | ``` 138 | 139 | ## Custom Security 140 | 141 | ```kotlin 142 | implementation("tech.thdev:useful-encrypted-data-store-preferences-security:$lastVersion") 143 | ``` 144 | 145 | Implement UsefulSecurity inheritance. 146 | 147 | ```kotlin 148 | class CustomSecurityImpl : UsefulSecurity { 149 | 150 | override fun encryptData(data: String): String { 151 | TODO("Not yet implemented") 152 | } 153 | 154 | override fun decryptData(encryptData: String): String { 155 | TODO("Not yet implemented") 156 | } 157 | } 158 | ``` 159 | 160 | ## The KSP path must be specified as required. 161 | 162 | module gradle. 163 | 164 | ```kotlin 165 | buildTypes { 166 | sourceSets.getByName("debug") { 167 | kotlin.srcDir("build/generated/ksp/debug/kotlin") 168 | } 169 | sourceSets.getByName("release") { 170 | kotlin.srcDir("build/generated/ksp/release/kotlin") 171 | } 172 | } 173 | ``` 174 | 175 | ## Result 176 | sample 177 | 178 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## 2.0.21-1.0.28-1.2.0 2 | 3 | - Library update. 4 | - Kotlin 2.0.21 5 | - KSP 2.0.21-1.0.28 6 | 7 | ## 1.9.25-1.0.20-1.2.0 8 | 9 | - release 1.2.0 10 | - ETC 11 | - Library update. 12 | - Kotlin 1.9.25 13 | - KSP 1.9.25-1.0.20 14 | 15 | ## 1.9.10-1.0.13-1.2.0-alpha03 16 | 17 | - Library update. 18 | - Kotlin 1.9.10 19 | - KSP 1.9.10-1.0.13 20 | 21 | ## 1.9.0-1.0.13-1.2.0-alpha02 22 | 23 | - Performance improvement 24 | - Split keystore value. 25 | - Change ecb. 26 | - Fix app First start decrypt fail. 27 | 28 | ## 1.9.0-1.0.13-1.2.0-alpha01 29 | 30 | - Secure change. Use AndroidKeyStore 31 | 32 | ## 1.7.21-1.0.8-1.1.0-alpha01 33 | 34 | - Add defaultValue 35 | - Default using String value. 36 | - Int -> "0" 37 | - Double -> "0.0" 38 | - String -> "message" 39 | - Boolean -> "true" or "false" 40 | - Float -> "0.0" 41 | - Long -> "0" 42 | 43 | ## 1.7.21-1.0.8-1.0.0 44 | 45 | - Library update. 46 | 47 | ## 1.0.0-alpha03 48 | 49 | - New annotation add. 50 | - @ClearValues - Use suspend function 51 | ```kotlin 52 | @ClearValues 53 | suspend fun clearAll() 54 | ``` 55 | - security map event change. 56 | - empty value stream default 57 | 58 | ## 1.0.0-alpha01 59 | 60 | - DataStorePreferences ksp module 61 | -------------------------------------------------------------------------------- /SamplePreferenceRepository/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /SamplePreferenceRepository/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("com.google.devtools.ksp") 6 | 7 | kotlin("android") 8 | } 9 | 10 | android { 11 | namespace = "tech.thdev.samplepreference.repository" 12 | 13 | buildToolsVersion = libs.versions.buildToolsVersion.get() 14 | compileSdk = libs.versions.compileSdk.get().toInt() 15 | 16 | defaultConfig { 17 | minSdk = libs.versions.minSdk.get().toInt() 18 | setCompileSdkVersion(libs.versions.targetSdk.get().toInt()) 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | vectorDrawables { 23 | useSupportLibrary = true 24 | } 25 | } 26 | 27 | buildTypes { 28 | getByName("debug") { 29 | isMinifyEnabled = false 30 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 31 | } 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_17 36 | targetCompatibility = JavaVersion.VERSION_17 37 | } 38 | 39 | kotlinOptions { 40 | jvmTarget = "17" 41 | } 42 | 43 | buildTypes { 44 | sourceSets.getByName("debug") { 45 | kotlin.srcDir("build/generated/ksp/debug/kotlin") 46 | } 47 | sourceSets.getByName("release") { 48 | kotlin.srcDir("build/generated/ksp/release/kotlin") 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation(libs.androidx.dataStorePreferences) 55 | implementation(libs.androidx.securityCrypto) 56 | 57 | // use - current release version 58 | // ksp(libs.usefulDataStorePreference.ksp) 59 | // implementation(libs.usefulDataStorePreference.ksp.annotation) 60 | // implementation(libs.usefulDataStorePreference.security) 61 | 62 | // use - local 63 | ksp(projects.usefulEncryptedDataStorePreferencesKsp) 64 | implementation(projects.usefulEncryptedDataStorePreferencesKspAnnotations) 65 | implementation(projects.usefulEncryptedDataStorePreferencesSecurity) 66 | } -------------------------------------------------------------------------------- /SamplePreferenceRepository/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.kts. 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 -------------------------------------------------------------------------------- /SamplePreferenceRepository/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /SamplePreferenceRepository/src/main/java/tech/thdev/samplepreference/repository/NonSecurePreferences.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.samplepreference.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 5 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 6 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 8 | 9 | @UsefulPreferences(disableSecure = true) 10 | interface NonSecurePreferences { 11 | 12 | @GetValue(KEY_INT, "100") 13 | fun getInt(): Flow 14 | 15 | @GetValue(KEY_INT, "100") 16 | suspend fun getIntValue(): Int 17 | 18 | @SetValue(KEY_INT) 19 | suspend fun setInt(value: Int) 20 | 21 | @GetValue(KEY_DOUBLE, "0.14") 22 | fun getDouble(): Flow 23 | 24 | @SetValue(KEY_DOUBLE) 25 | suspend fun setDouble(value: Double) 26 | 27 | @GetValue(KEY_STRING) 28 | fun getString(): Flow 29 | 30 | @SetValue(KEY_STRING) 31 | suspend fun setString(value: String) 32 | 33 | @GetValue(KEY_BOOLEAN, "true") 34 | fun getBoolean(): Flow 35 | 36 | @SetValue(KEY_BOOLEAN) 37 | suspend fun setBoolean(value: Boolean) 38 | 39 | @GetValue(KEY_FLOAT) 40 | fun getFloat(): Flow 41 | 42 | @SetValue(KEY_FLOAT) 43 | suspend fun setFloat(value: Float) 44 | 45 | @GetValue(KEY_LONG) 46 | fun getLong(): Flow 47 | 48 | @SetValue(KEY_LONG) 49 | suspend fun setLong(value: Long) 50 | 51 | @ClearValues 52 | suspend fun clearAll() 53 | 54 | companion object { 55 | 56 | private const val KEY_INT = "key-int" 57 | private const val KEY_DOUBLE = "key-double" 58 | private const val KEY_STRING = "key-string" 59 | private const val KEY_BOOLEAN = "key-boolean" 60 | private const val KEY_FLOAT = "key-float" 61 | private const val KEY_LONG = "key-long" 62 | } 63 | } -------------------------------------------------------------------------------- /SamplePreferenceRepository/src/main/java/tech/thdev/samplepreference/repository/SecurePreferences.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.samplepreference.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 5 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 6 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 8 | 9 | @UsefulPreferences 10 | interface SecurePreferences { 11 | 12 | @GetValue(KEY_INT, "200") 13 | fun flowInt(): Flow 14 | 15 | @GetValue(KEY_INT, "200") 16 | suspend fun getInt(): Int 17 | 18 | @SetValue(KEY_INT) 19 | suspend fun setInt(value: Int) 20 | 21 | @GetValue(KEY_DOUBLE, "0.0153") 22 | fun flowDouble(): Flow 23 | 24 | @SetValue(KEY_DOUBLE) 25 | suspend fun setDouble(value: Double) 26 | 27 | @GetValue(KEY_STRING, "default string") 28 | fun flowString(): Flow 29 | 30 | @SetValue(KEY_STRING) 31 | suspend fun setString(value: String) 32 | 33 | @GetValue(KEY_BOOLEAN, "true") 34 | fun flowBoolean(): Flow 35 | 36 | @SetValue(KEY_BOOLEAN) 37 | suspend fun setBoolean(value: Boolean) 38 | 39 | @GetValue(KEY_FLOAT, "1234.11") 40 | fun flowFloat(): Flow 41 | 42 | @SetValue(KEY_FLOAT) 43 | suspend fun setFloat(value: Float) 44 | 45 | @GetValue(KEY_LONG, "949") 46 | fun flowLong(): Flow 47 | 48 | @SetValue(KEY_LONG) 49 | suspend fun setLong(value: Long) 50 | 51 | @ClearValues 52 | suspend fun clearAll() 53 | 54 | companion object { 55 | 56 | private const val KEY_INT = "key-int" 57 | private const val KEY_DOUBLE = "key-double" 58 | private const val KEY_STRING = "key-string" 59 | private const val KEY_BOOLEAN = "key-boolean" 60 | private const val KEY_FLOAT = "key-float" 61 | private const val KEY_LONG = "key-long" 62 | } 63 | } -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.application") 5 | kotlin("android") 6 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" // this version matches your Kotlin version 7 | } 8 | 9 | android { 10 | namespace = "tech.thdev.encrypteddatastorepreference" 11 | 12 | buildToolsVersion = libs.versions.buildToolsVersion.get() 13 | compileSdk = libs.versions.compileSdk.get().toInt() 14 | 15 | defaultConfig { 16 | minSdk = libs.versions.minSdk.get().toInt() 17 | targetSdk = libs.versions.targetSdk.get().toInt() 18 | versionCode = libs.versions.versionCode.get().toInt() 19 | versionName = "${libs.versions.major.get()}.${libs.versions.minor.get()}.${libs.versions.hotfix.get()}" 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | 23 | vectorDrawables { 24 | useSupportLibrary = true 25 | } 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 32 | } 33 | 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_17 42 | targetCompatibility = JavaVersion.VERSION_17 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "17" 47 | } 48 | 49 | buildFeatures { 50 | compose = true 51 | } 52 | 53 | packaging { 54 | resources.excludes.addAll( 55 | listOf( 56 | "META-INF/AL2.0", 57 | "META-INF/LGPL2.1", 58 | ) 59 | ) 60 | } 61 | } 62 | 63 | dependencies { 64 | implementation(libs.androidx.coreKtx) 65 | implementation(libs.androidx.lifecycleCommonJava8) 66 | implementation(libs.androidx.dataStorePreferences) 67 | implementation(libs.androidx.securityCrypto) 68 | implementation(libs.compose.activity) 69 | implementation(libs.compose.ui) 70 | implementation(libs.compose.uiToolingPreview) 71 | implementation(libs.compose.material) 72 | 73 | debugImplementation(libs.compose.uiTooling) 74 | debugImplementation(libs.compose.ui) 75 | 76 | implementation(projects.samplePreferenceRepository) 77 | 78 | // use - local 79 | implementation(projects.usefulEncryptedDataStorePreferencesSecurity) 80 | } -------------------------------------------------------------------------------- /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.kts. 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 -------------------------------------------------------------------------------- /app/src/androidTest/java/tech/thdev/encrypteddatastorepreference/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("tech.thdev.encrypteddatastorepreference", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/CompositionLocalProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | 6 | @Composable 7 | internal fun SecureSampleActivity.ComposeLocalProvider(content: @Composable () -> Unit) { 8 | CompositionLocalProvider( 9 | mainViewModelProvider(viewModel), 10 | content = content 11 | ) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/LocalMainViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ProvidableCompositionLocal 5 | import androidx.compose.runtime.compositionLocalOf 6 | 7 | private val LocalSecureSampleViewModel: ProvidableCompositionLocal = 8 | compositionLocalOf { null } 9 | 10 | internal fun mainViewModelProvider(developViewModel: SecureSampleViewModel) = 11 | LocalSecureSampleViewModel provides developViewModel 12 | 13 | internal val localSecureSampleViewModel: SecureSampleViewModel 14 | @Composable 15 | get() = LocalSecureSampleViewModel.current!! -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/SecureSampleActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.viewModels 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider 11 | import tech.thdev.encrypteddatastorepreference.compose.SecureSampleScreen 12 | import tech.thdev.encrypteddatastorepreference.ui.theme.EncryptedDataStorePreferenceTheme 13 | import tech.thdev.samplepreference.repository.SecurePreferences 14 | import tech.thdev.samplepreference.repository.generateSecurePreferences 15 | import tech.thdev.useful.encrypted.data.store.preferences.security.generateUsefulSecurity 16 | 17 | class SecureSampleActivity : ComponentActivity() { 18 | 19 | private val Context.dataStore by preferencesDataStore(name = "security-preference") 20 | 21 | private val securityPreference: SecurePreferences by lazy { 22 | dataStore.generateSecurePreferences( 23 | generateUsefulSecurity( 24 | keyStoreAlias = packageName, 25 | ) 26 | ) 27 | } 28 | 29 | @Suppress("UNCHECKED_CAST") 30 | internal val viewModel by viewModels { 31 | object : ViewModelProvider.Factory { 32 | override fun create(modelClass: Class): T { 33 | return SecureSampleViewModel(securityPreference) as T 34 | } 35 | } 36 | } 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | setContent { 41 | ComposeLocalProvider { 42 | EncryptedDataStorePreferenceTheme { 43 | SecureSampleScreen() 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/SecureSampleViewModel.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.mutableDoubleStateOf 5 | import androidx.compose.runtime.mutableFloatStateOf 6 | import androidx.compose.runtime.mutableIntStateOf 7 | import androidx.compose.runtime.mutableLongStateOf 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import kotlinx.coroutines.flow.catch 12 | import kotlinx.coroutines.flow.launchIn 13 | import kotlinx.coroutines.flow.onEach 14 | import kotlinx.coroutines.launch 15 | import tech.thdev.samplepreference.repository.SecurePreferences 16 | 17 | class SecureSampleViewModel( 18 | private val securityPreference: SecurePreferences, 19 | ) : ViewModel() { 20 | 21 | val intValue = mutableIntStateOf(0) 22 | val doubleValue = mutableDoubleStateOf(0.0) 23 | val floatValue = mutableFloatStateOf(0.0f) 24 | val stringValue = mutableStateOf("") 25 | val booleanValue = mutableStateOf(false) 26 | val longValue = mutableLongStateOf(0L) 27 | 28 | init { 29 | securityPreference.flowInt() 30 | .onEach { 31 | Log.d("DataStore", "Current Int $it") 32 | intValue.intValue = it 33 | } 34 | .catch { 35 | it.printStackTrace() 36 | } 37 | .launchIn(viewModelScope) 38 | 39 | securityPreference.flowDouble() 40 | .onEach { 41 | Log.d("DataStore", "Current Double $it") 42 | doubleValue.doubleValue = it 43 | } 44 | .catch { 45 | it.printStackTrace() 46 | } 47 | .launchIn(viewModelScope) 48 | 49 | securityPreference.flowString() 50 | .onEach { 51 | Log.d("DataStore", "Current String $it") 52 | stringValue.value = it 53 | } 54 | .catch { 55 | it.printStackTrace() 56 | } 57 | .launchIn(viewModelScope) 58 | 59 | securityPreference.flowBoolean() 60 | .onEach { 61 | Log.d("DataStore", "Current Boolean $it") 62 | booleanValue.value = it 63 | } 64 | .catch { 65 | it.printStackTrace() 66 | } 67 | .launchIn(viewModelScope) 68 | 69 | securityPreference.flowFloat() 70 | .onEach { 71 | Log.d("DataStore", "Current Float $it") 72 | floatValue.floatValue = it 73 | } 74 | .catch { 75 | it.printStackTrace() 76 | } 77 | .launchIn(viewModelScope) 78 | 79 | securityPreference.flowLong() 80 | .onEach { 81 | Log.d("DataStore", "Current Long $it") 82 | longValue.longValue = it 83 | } 84 | .catch { 85 | it.printStackTrace() 86 | } 87 | .launchIn(viewModelScope) 88 | } 89 | 90 | fun intValueChange() = viewModelScope.launch { 91 | securityPreference.setInt(++intValue.intValue) 92 | } 93 | 94 | fun doubleValueChange() = viewModelScope.launch { 95 | securityPreference.setDouble(++doubleValue.doubleValue) 96 | } 97 | 98 | fun floatValueChange() = viewModelScope.launch { 99 | securityPreference.setFloat(++floatValue.floatValue) 100 | } 101 | 102 | fun stringValueChange() = viewModelScope.launch { 103 | securityPreference.setString("New value ${intValue.intValue}") 104 | } 105 | 106 | fun booleanValueChange() = viewModelScope.launch { 107 | securityPreference.setBoolean(booleanValue.value.not()) 108 | } 109 | 110 | fun longValueChange() = viewModelScope.launch { 111 | securityPreference.setLong(++longValue.longValue) 112 | } 113 | 114 | fun clear() = viewModelScope.launch { 115 | securityPreference.clearAll() 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/compose/SecureSampleScreen.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.compose 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material.Button 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.State 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import tech.thdev.encrypteddatastorepreference.compose.component.TestItem 22 | import tech.thdev.encrypteddatastorepreference.localSecureSampleViewModel 23 | import tech.thdev.encrypteddatastorepreference.ui.theme.EncryptedDataStorePreferenceTheme 24 | 25 | @Composable 26 | fun MutableState.asRemember(): State { 27 | return remember { this } 28 | } 29 | 30 | @Composable 31 | internal fun SecureSampleScreen() { 32 | val intValue by localSecureSampleViewModel.intValue.asRemember() 33 | val doubleValue by localSecureSampleViewModel.doubleValue.asRemember() 34 | val floatValue by localSecureSampleViewModel.floatValue.asRemember() 35 | val stringValue by localSecureSampleViewModel.stringValue.asRemember() 36 | val booleanValue by localSecureSampleViewModel.booleanValue.asRemember() 37 | val longValue by localSecureSampleViewModel.longValue.asRemember() 38 | 39 | val viewModel = localSecureSampleViewModel 40 | SecureSampleScreen( 41 | intValue = intValue, 42 | onClickChangeInt = { 43 | viewModel.intValueChange() 44 | }, 45 | doubleValue = doubleValue, 46 | onClickChangeDouble = { 47 | viewModel.doubleValueChange() 48 | }, 49 | floatValue = floatValue, 50 | onClickChangeFloat = { 51 | viewModel.floatValueChange() 52 | }, 53 | stringValue = stringValue, 54 | onClickChangeString = { 55 | viewModel.stringValueChange() 56 | }, 57 | booleanValue = booleanValue, 58 | onClickChangeBoolean = { 59 | viewModel.booleanValueChange() 60 | }, 61 | longValue = longValue, 62 | onClickChangeLong = { 63 | viewModel.longValueChange() 64 | }, 65 | onClickClean = { 66 | viewModel.clear() 67 | } 68 | ) 69 | } 70 | 71 | @Composable 72 | internal fun SecureSampleScreen( 73 | intValue: Int, 74 | onClickChangeInt: () -> Unit, 75 | doubleValue: Double, 76 | onClickChangeDouble: () -> Unit, 77 | floatValue: Float, 78 | onClickChangeFloat: () -> Unit, 79 | stringValue: String, 80 | onClickChangeString: () -> Unit, 81 | booleanValue: Boolean, 82 | onClickChangeBoolean: () -> Unit, 83 | longValue: Long, 84 | onClickChangeLong: () -> Unit, 85 | onClickClean: () -> Unit, 86 | ) { 87 | Column( 88 | modifier = Modifier 89 | .fillMaxSize() 90 | .padding(20.dp) 91 | .verticalScroll(rememberScrollState()) 92 | ) { 93 | Text( 94 | text = "Encrypted Preference test", 95 | style = TextStyle(fontSize = 20.sp) 96 | ) 97 | 98 | TestItem( 99 | text = "Int value $intValue", 100 | buttonText = "Int type", 101 | onClick = onClickChangeInt, 102 | ) 103 | 104 | TestItem( 105 | text = "Double value $doubleValue", 106 | buttonText = "Double type", 107 | onClick = onClickChangeDouble, 108 | ) 109 | 110 | TestItem( 111 | text = "String value $stringValue", 112 | buttonText = "String type", 113 | onClick = onClickChangeString, 114 | ) 115 | 116 | TestItem( 117 | text = "Boolean value $booleanValue", 118 | buttonText = "Boolean type", 119 | onClick = onClickChangeBoolean, 120 | ) 121 | 122 | TestItem( 123 | text = "Long value $longValue", 124 | buttonText = "Long type", 125 | onClick = onClickChangeLong, 126 | ) 127 | 128 | TestItem( 129 | text = "Float value $floatValue", 130 | buttonText = "Float type", 131 | onClick = onClickChangeFloat, 132 | ) 133 | 134 | Button( 135 | onClick = onClickClean, 136 | modifier = Modifier 137 | .fillMaxWidth() 138 | .padding(top = 10.dp) 139 | ) { 140 | Text(text = "Clear value") 141 | } 142 | } 143 | } 144 | 145 | @Preview 146 | @Composable 147 | internal fun PreviewSecureSampleScreen() { 148 | EncryptedDataStorePreferenceTheme { 149 | SecureSampleScreen( 150 | intValue = 0, 151 | onClickChangeInt = {}, 152 | doubleValue = 0.123, 153 | onClickChangeDouble = {}, 154 | floatValue = 0.12F, 155 | onClickChangeFloat = {}, 156 | stringValue = "string value", 157 | onClickChangeString = {}, 158 | booleanValue = true, 159 | onClickChangeBoolean = {}, 160 | longValue = 1234, 161 | onClickChangeLong = {}, 162 | onClickClean = {} 163 | ) 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/compose/component/TestComponent.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.compose.component 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | import tech.thdev.encrypteddatastorepreference.ui.theme.EncryptedDataStorePreferenceTheme 15 | 16 | @Composable 17 | internal fun TestItem( 18 | text: String, 19 | buttonText: String, 20 | onClick: () -> Unit, 21 | ) { 22 | Column( 23 | modifier = Modifier 24 | .padding(top = 10.dp) 25 | ) { 26 | Text( 27 | text = text, 28 | style = TextStyle(fontSize = 14.sp), 29 | modifier = Modifier 30 | .fillMaxWidth() 31 | ) 32 | Button( 33 | onClick = onClick, 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | ) { 37 | Text(text = buttonText) 38 | } 39 | } 40 | } 41 | 42 | @Preview 43 | @Composable 44 | internal fun PreviewTestItem() { 45 | EncryptedDataStorePreferenceTheme { 46 | TestItem( 47 | text = "Text", 48 | buttonText = "Button", 49 | onClick = {}, 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun EncryptedDataStorePreferenceTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = Typography, 41 | shapes = Shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/tech/thdev/encrypteddatastorepreference/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | EncryptedDataStorePreference 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/tech/thdev/encrypteddatastorepreference/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.encrypteddatastorepreference 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath(libs.plugin.androidGradlePlugin) 9 | classpath(libs.plugin.kotlin) 10 | classpath(libs.plugin.ksp) 11 | } 12 | } 13 | 14 | plugins { 15 | alias(libs.plugins.compose.compiler) apply false 16 | } 17 | 18 | allprojects { 19 | tasks.withType { 20 | kotlinOptions.jvmTarget = "17" 21 | 22 | kotlinOptions.allWarningsAsErrors = false 23 | 24 | kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 25 | kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.Experimental" 26 | } 27 | } 28 | 29 | tasks.register("clean", Delete::class) { 30 | delete(rootProject.buildDir) 31 | } -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | `kotlin-dsl-precompiled-script-plugins` 6 | } 7 | 8 | val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks 9 | compileKotlin.kotlinOptions { 10 | jvmTarget = "17" 11 | } 12 | 13 | dependencies { 14 | implementation(libs.plugin.androidGradlePlugin) 15 | implementation(libs.plugin.kotlin) 16 | implementation(libs.plugin.ksp) 17 | } -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | @Suppress("UnstableApiUsage") 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | 13 | versionCatalogs { 14 | create("libs") { 15 | from(files("../gradle/libs.versions.toml")) 16 | } 17 | } 18 | 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tech/thdev/gradle/dependencies/Publish.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.gradle.dependencies 2 | 3 | object Publish { 4 | const val libraryVersion = "1.9.10-1.0.13-1.2.0-alpha03" 5 | const val description = "Android Encrypted DataStorePreferences" 6 | const val publishUrl = "https://thdev.tech/EncryptedDataStorePreference/" 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tech/thdev/gradle/locals/base/lib-publish-android.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.api.AndroidSourceSet 2 | import org.jetbrains.kotlin.konan.properties.Properties 3 | 4 | plugins { 5 | id("com.android.library") 6 | id("maven-publish") 7 | id("signing") 8 | } 9 | 10 | // Stub secrets to let the project sync and build without the publication values set up 11 | ext["signing.keyId"] = "" 12 | ext["signing.password"] = "" 13 | ext["signing.key"] = "" 14 | ext["ossrhUsername"] = "" 15 | ext["ossrhPassword"] = "" 16 | 17 | val javadocJar by tasks.registering(Jar::class) { 18 | archiveClassifier.set("javadoc") 19 | } 20 | 21 | val androidSourceJar by tasks.registering(Jar::class) { 22 | archiveClassifier.set("sources") 23 | from(android.sourceSets.getByName("main").java.srcDirs) 24 | } 25 | 26 | fun getExtraString(name: String) = ext[name]?.toString() 27 | 28 | fun groupId(): String = "tech.thdev" 29 | 30 | afterEvaluate { 31 | // Grabbing secrets from local.properties file or from environment variables, which could be used on CI 32 | val secretPropsFile = project.rootProject.file("local.properties") 33 | if (secretPropsFile.exists()) { 34 | secretPropsFile.reader().use { 35 | Properties().apply { 36 | load(it) 37 | } 38 | }.onEach { (name, value) -> 39 | ext[name.toString()] = value 40 | } 41 | } else { 42 | // Use system environment variables 43 | ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME") 44 | ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD") 45 | ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID") 46 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD") 47 | ext["signing.key"] = System.getenv("SIGNING_KEY") 48 | } 49 | 50 | // Set up Sonatype repository 51 | publishing { 52 | val artifactName = getExtraString("libraryName") ?: name 53 | val libraryVersion = getExtraString("libraryVersion") ?: "DEV" 54 | val artifactDescription = getExtraString("description") ?: "" 55 | val artifactUrl: String = getExtraString("url") ?: "http://thdev.tech/" 56 | 57 | println("artifactName $artifactName") 58 | println("libraryVersion $libraryVersion") 59 | println("artifactDescription $artifactDescription") 60 | println("artifactUrl $artifactUrl") 61 | 62 | // Configure maven central repository 63 | repositories { 64 | maven { 65 | name = "sonatype" 66 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 67 | credentials { 68 | username = getExtraString("ossrhUsername") 69 | password = getExtraString("ossrhPassword") 70 | } 71 | } 72 | } 73 | 74 | // Configure all publications 75 | publications { 76 | create("release") { 77 | groupId = groupId() 78 | artifactId = artifactName 79 | version = libraryVersion 80 | 81 | if (project.plugins.hasPlugin("com.android.library")) { 82 | from(components.getByName("release")) 83 | } else { 84 | from(components.getByName("java")) 85 | } 86 | 87 | // Stub android 88 | artifact(androidSourceJar.get()) 89 | // Stub javadoc.jar artifact 90 | artifact(javadocJar.get()) 91 | 92 | // Provide artifacts information requited by Maven Central 93 | pom { 94 | name.set(artifactName) 95 | description.set(artifactDescription) 96 | url.set(artifactUrl) 97 | 98 | licenses { 99 | license { 100 | name.set("The Apache License, Version 2.0") 101 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 102 | } 103 | } 104 | developers { 105 | developer { 106 | id.set("taehwandev") 107 | name.set("taehwan") 108 | email.set("develop@thdev.tech") 109 | } 110 | } 111 | scm { 112 | url.set(artifactUrl) 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | // Signing artifacts. Signing.* extra properties values will be used 120 | signing { 121 | useInMemoryPgpKeys( 122 | getExtraString("signing.keyId"), 123 | getExtraString("signing.key"), 124 | getExtraString("signing.password"), 125 | ) 126 | sign(publishing.publications) 127 | } 128 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/tech/thdev/gradle/locals/base/lib-publish.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.konan.properties.Properties 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("maven-publish") 6 | id("signing") 7 | } 8 | 9 | // Stub secrets to let the project sync and build without the publication values set up 10 | ext["signing.keyId"] = "" 11 | ext["signing.password"] = "" 12 | ext["signing.key"] = "" 13 | ext["ossrhUsername"] = "" 14 | ext["ossrhPassword"] = "" 15 | 16 | val javadocJar by tasks.registering(Jar::class) { 17 | archiveClassifier.set("javadoc") 18 | } 19 | 20 | val sourceJar by tasks.registering(Jar::class) { 21 | archiveClassifier.set("sources") 22 | from(sourceSets.main.get().allSource) 23 | } 24 | 25 | fun getExtraString(name: String) = ext[name]?.toString() 26 | 27 | fun groupId(): String = "tech.thdev" 28 | 29 | afterEvaluate { 30 | // Grabbing secrets from local.properties file or from environment variables, which could be used on CI 31 | val secretPropsFile = project.rootProject.file("local.properties") 32 | if (secretPropsFile.exists()) { 33 | secretPropsFile.reader().use { 34 | Properties().apply { 35 | load(it) 36 | } 37 | }.onEach { (name, value) -> 38 | ext[name.toString()] = value 39 | } 40 | } else { 41 | // Use system environment variables 42 | ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME") 43 | ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD") 44 | ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID") 45 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD") 46 | ext["signing.key"] = System.getenv("SIGNING_KEY") 47 | } 48 | 49 | // Set up Sonatype repository 50 | publishing { 51 | val artifactName = getExtraString("libraryName") ?: name 52 | val libraryVersion = getExtraString("libraryVersion") ?: "DEV" 53 | val artifactDescription = getExtraString("description") ?: "" 54 | val artifactUrl: String = getExtraString("url") ?: "http://thdev.tech/" 55 | 56 | println("artifactName $artifactName") 57 | println("libraryVersion $libraryVersion") 58 | println("artifactDescription $artifactDescription") 59 | println("artifactUrl $artifactUrl") 60 | 61 | // Configure maven central repository 62 | repositories { 63 | maven { 64 | name = "sonatype" 65 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 66 | credentials { 67 | username = getExtraString("ossrhUsername") 68 | password = getExtraString("ossrhPassword") 69 | } 70 | } 71 | } 72 | 73 | // Configure all publications 74 | publications { 75 | create("release") { 76 | groupId = groupId() 77 | artifactId = artifactName 78 | version = libraryVersion 79 | 80 | if (project.plugins.hasPlugin("com.android.library")) { 81 | from(components.getByName("release")) 82 | } else { 83 | from(components.getByName("java")) 84 | } 85 | 86 | // Stub android 87 | artifact(sourceJar.get()) 88 | // Stub javadoc.jar artifact 89 | artifact(javadocJar.get()) 90 | 91 | // Provide artifacts information requited by Maven Central 92 | pom { 93 | name.set(artifactName) 94 | description.set(artifactDescription) 95 | url.set(artifactUrl) 96 | 97 | licenses { 98 | license { 99 | name.set("The Apache License, Version 2.0") 100 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 101 | } 102 | } 103 | developers { 104 | developer { 105 | id.set("taehwandev") 106 | name.set("taehwan") 107 | email.set("develop@thdev.tech") 108 | } 109 | } 110 | scm { 111 | url.set(artifactUrl) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | // Signing artifacts. Signing.* extra properties values will be used 119 | signing { 120 | useInMemoryPgpKeys( 121 | getExtraString("signing.keyId"), 122 | getExtraString("signing.key"), 123 | getExtraString("signing.password"), 124 | ) 125 | sign(publishing.publications) 126 | } 127 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | buildToolsVersion = "35.0.0" 3 | compileSdk = "35" 4 | targetSdk = "35" 5 | minSdk = "23" 6 | 7 | major = "1" 8 | minor = "0" 9 | hotfix = "0" 10 | versionCode = "1" 11 | 12 | ## Publish version 13 | libraryVersion = "2.0.21-1.0.28-1.2.0" 14 | 15 | ## Android gradle plugin 16 | androidGradlePlugin = "8.8.0" 17 | 18 | ## UsefulDataStorePreference 19 | usefulDataStorePreference = "1.0.0-alpha04" 20 | 21 | ## kotlin 22 | # https://github.com/JetBrains/kotlin 23 | kotlin = "2.0.21" 24 | 25 | ## Coroutine 26 | # https://github.com/Kotlin/kotlinx.coroutines 27 | coroutines = "1.10.1" 28 | # https://github.com/cashapp/turbine 29 | coroutines-turbine = "1.2.0" 30 | 31 | ## KSP 32 | # https://github.com/google/ksp 33 | ksp = "2.0.21-1.0.28" 34 | # https://github.com/square/kotlinpoet 35 | kotlinPoet = "2.0.0" 36 | # https://github.com/tschuchortdev/kotlin-compile-testing 37 | kspTestVersion = "1.6.0" 38 | 39 | ## AndroidX 40 | # https://developer.android.com/jetpack/androidx/releases/annotation 41 | androidx-annotation = "1.9.1" 42 | # https://developer.android.com/jetpack/androidx/releases/core 43 | androidx-coreKtx = "1.15.0" 44 | # https://developer.android.com/jetpack/androidx/releases/appcompat 45 | androidx-appCompat = "1.7.0" 46 | # https://developer.android.com/jetpack/androidx/releases/activity 47 | androidx-activity = "1.9.3" 48 | # https://developer.android.com/jetpack/androidx/releases/vectordrawable 49 | androidx-vectorDrawable = "1.2.0" 50 | # https://developer.android.com/jetpack/androidx/releases/lifecycle 51 | androidx-lifecycleVersion = "2.8.7" 52 | # https://developer.android.com/jetpack/androidx/releases/security 53 | androidx-securityCrypto = "1.1.0-alpha06" 54 | # https://developer.android.com/jetpack/androidx/releases/datastore 55 | androidx-dataStorePreferences = "1.1.2" 56 | 57 | ## Compose 58 | # https://developer.android.com/jetpack/androidx/releases/compose 59 | compose = "1.7.6" 60 | # https://developer.android.com/jetpack/androidx/releases/compose-material3 61 | compose-material3 = "1.3.1" 62 | 63 | ## Test 64 | # https://developer.android.com/jetpack/androidx/releases/test 65 | test-androidx-core = "1.6.1" 66 | test-androidx-runner = "1.6.2" 67 | test-androidx-junit = "1.2.1" 68 | # https://github.com/robolectric/robolectric 69 | test-robolectric = "4.14.1" 70 | # https://github.com/mockito/mockito 71 | test-mockito = "5.14.2" 72 | # https://github.com/mockito/mockito-kotlin 73 | test-mockito-kotlin = "5.4.0" 74 | # https://github.com/mannodermaus/android-junit5 75 | test-junit5 = "5.11.3" 76 | 77 | [libraries] 78 | ## usefulDataStorePreference 79 | usefulDataStorePreference-ksp = { module = "tech.thdev:useful-encrypted-data-store-preferences-ksp", version.ref = "usefulDataStorePreference"} 80 | usefulDataStorePreference-ksp-annotations = { module = "tech.thdev:useful-encrypted-data-store-preferences-ksp-annotations", version.ref = "usefulDataStorePreference"} 81 | usefulDataStorePreference-security = { module = "tech.thdev:useful-encrypted-data-store-preferences-security", version.ref = "usefulDataStorePreference"} 82 | 83 | ## Kotlin 84 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 85 | kotlin-reflection = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 86 | 87 | ## Coroutines 88 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 89 | 90 | ## KSP 91 | ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } 92 | ksp-kotlinPoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoet" } 93 | 94 | ## AndroidX 95 | androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } 96 | androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidx-coreKtx" } 97 | androidx-appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appCompat" } 98 | androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } 99 | androidx-lifecycleCommonJava8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycleVersion" } 100 | androidx-vectorDrawable = { module = "androidx.vectordrawable:vectordrawable", version.ref = "androidx-vectorDrawable" } 101 | androidx-securityCrypto = { module = "androidx.security:security-crypto", version.ref = "androidx-securityCrypto" } 102 | androidx-dataStorePreferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-dataStorePreferences" } 103 | androidx-dataStorePreferencesCore = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-dataStorePreferences" } 104 | 105 | ## Compose 106 | compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } 107 | compose-uiTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 108 | compose-uiToolingPreview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 109 | compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } 110 | compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } 111 | compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } 112 | compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } 113 | compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } 114 | compose-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } 115 | 116 | ## Test 117 | test-androidx-core = { module = "androidx.test:core", version.ref = "test-androidx-core" } 118 | test-androidx-runner = { module = "androidx.test:runner", version.ref = "test-androidx-runner" } 119 | test-androidx-junit = { module = "androidx.test.ext:junit", version.ref = "test-androidx-junit" } 120 | 121 | ## Robolectric 122 | test-robolectric = { module = "org.robolectric:robolectric", version.ref = "test-robolectric" } 123 | test-robolectric-shadowapi = { module = "org.robolectric:shadowapi", version.ref = "test-robolectric" } 124 | 125 | ## Test coroutine 126 | test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 127 | test-coroutines-turbine = { module = "app.cash.turbine:turbine", version.ref = "coroutines-turbine" } 128 | 129 | ## Test KSP 130 | test-kotlinCompilTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kspTestVersion" } 131 | test-kotlinCompilTestingKSP = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "kspTestVersion" } 132 | 133 | ## Test mockito 134 | test-mockito = { module = "org.mockito:mockito-core", version.ref = "test-mockito" } 135 | test-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "test-mockito" } 136 | test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "test-mockito-kotlin" } 137 | 138 | ## Test junit 5 139 | test-junit5 = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "test-junit5" } 140 | test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "test-junit5" } 141 | test-junit5-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "test-junit5" } 142 | test-junit5-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "test-junit5" } 143 | 144 | # plugin 145 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 146 | plugin-ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } 147 | plugin-androidGradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } 148 | 149 | [plugins] 150 | androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" } 151 | androidApp = { id = "com.android.application", version.ref = "androidGradlePlugin" } 152 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 153 | kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 154 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 155 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 22 12:40:12 KST 2022 2 | # https://gradle.org/releases/ 3 | distributionBase=GRADLE_USER_HOME 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | distributionPath=wrapper/dists 6 | zipStorePath=wrapper/dists 7 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | mavenCentral() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 19 | 20 | rootProject.name = "EncryptedDataStorePreference" 21 | // Sample 22 | include( 23 | ":app", 24 | ":SamplePreferenceRepository", 25 | ) 26 | 27 | // library 28 | include( 29 | ":useful-encrypted-data-store-preferences-security", 30 | ":useful-encrypted-data-store-preferences-ksp", 31 | ":useful-encrypted-data-store-preferences-ksp-annotations", 32 | ) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import tech.thdev.gradle.dependencies.Publish 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("lib-publish") 6 | } 7 | 8 | java { 9 | sourceCompatibility = JavaVersion.VERSION_17 10 | targetCompatibility = JavaVersion.VERSION_17 11 | } 12 | 13 | ext["libraryName"] = "useful-encrypted-data-store-preferences-ksp-annotations" 14 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 15 | ext["description"] = Publish.description 16 | ext["url"] = Publish.publishUrl -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/annotations/UsefulPreferences.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations 2 | 3 | /** 4 | * Use DataStorePreference 5 | * 6 | * Use UsefulPreference. 7 | * 8 | * Sample code 9 |
10 | @UsefulPreferences(/* options disableSecurity = true */)
11 | interface XXXPreferences {
12 | 
13 | @GetValue(KEY_INT)
14 | fun getInt(): Flow
15 | // oro
16 | @GetValue(KEY_INT)
17 | 
18 | 
19 | @SetValue(KEY_INT)
20 | suspend fun setInt(value: Int)
21 | 
22 | @GetValue(KEY_DOUBLE)
23 | fun getDouble(): Flow
24 | 
25 | @SetValue(KEY_DOUBLE)
26 | suspend fun setDouble(value: Double)
27 | 
28 | @GetValue(KEY_STRING)
29 | fun getString(): Flow
30 | 
31 | @SetValue(KEY_STRING)
32 | suspend fun setString(value: String)
33 | 
34 | @GetValue(KEY_BOOLEAN)
35 | fun getBoolean(): Flow
36 | 
37 | @SetValue(KEY_BOOLEAN)
38 | suspend fun setBoolean(value: Boolean)
39 | 
40 | @GetValue(KEY_FLOAT)
41 | fun getFloat(): Flow
42 | 
43 | @SetValue(KEY_FLOAT)
44 | suspend fun setFloat(value: Float)
45 | 
46 | @GetValue(KEY_LONG)
47 | fun getLong(): Flow
48 | 
49 | @SetValue(KEY_LONG)
50 | suspend fun setLong(value: Long)
51 | 
52 | companion object {
53 | 
54 | private const val KEY_INT = "key-int"
55 | private const val KEY_DOUBLE = "key-double"
56 | private const val KEY_STRING = "key-string"
57 | private const val KEY_BOOLEAN = "key-boolean"
58 | private const val KEY_FLOAT = "key-float"
59 | private const val KEY_LONG = "key-long"
60 | }
61 | }
62 | 
63 | */ 64 | @Retention(AnnotationRetention.SOURCE) 65 | @Target(AnnotationTarget.CLASS) 66 | annotation class UsefulPreferences(@Suppress("unused") val disableSecure: Boolean = false) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/annotations/value/ClearValues.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.FUNCTION) 5 | annotation class ClearValues -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/annotations/value/GetValue.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.FUNCTION) 5 | annotation class GetValue( 6 | /** 7 | * DataStore key 8 | */ 9 | @Suppress("unused") val key: String, 10 | /** 11 | * default value option. 12 | * only string. 13 | * 14 | * Int -> "0" 15 | * Double -> "0.0" 16 | * String -> "message" 17 | * Boolean -> "true" or "false" 18 | * Float -> "0.0" 19 | * Long -> "0" 20 | */ 21 | @Suppress("unused") val defaultValue: String = "", 22 | ) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp-annotations/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/annotations/value/SetValue.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.FUNCTION) 5 | annotation class SetValue(val key: String) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import tech.thdev.gradle.dependencies.Publish 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("lib-publish") 6 | } 7 | 8 | ext["libraryName"] = "useful-encrypted-data-store-preferences-ksp" 9 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 10 | ext["description"] = Publish.description 11 | ext["url"] = Publish.publishUrl 12 | 13 | java { 14 | sourceCompatibility = JavaVersion.VERSION_17 15 | targetCompatibility = JavaVersion.VERSION_17 16 | } 17 | 18 | tasks.withType() { 19 | useJUnitPlatform() 20 | } 21 | 22 | dependencies { 23 | implementation(libs.coroutines.core) 24 | implementation(libs.ksp) 25 | implementation(libs.ksp.kotlinPoet) 26 | 27 | implementation(projects.usefulEncryptedDataStorePreferencesKspAnnotations) 28 | 29 | testImplementation(libs.test.kotlinCompilTesting) 30 | testImplementation(libs.test.kotlinCompilTestingKSP) 31 | testImplementation(libs.test.junit5) 32 | testImplementation(libs.test.junit5.engine) 33 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/EncryptedDataStorePreferencesProcessor.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp 2 | 3 | import com.google.devtools.ksp.processing.CodeGenerator 4 | import com.google.devtools.ksp.processing.KSPLogger 5 | import com.google.devtools.ksp.processing.Resolver 6 | import com.google.devtools.ksp.processing.SymbolProcessor 7 | import com.google.devtools.ksp.symbol.KSAnnotated 8 | import com.google.devtools.ksp.symbol.KSClassDeclaration 9 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 10 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.generatePreferences 11 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.ResearchModel 12 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.visitor.findUsefulPreferences 13 | 14 | class EncryptedDataStorePreferencesProcessor( 15 | private val codeGenerator: CodeGenerator, 16 | private val logger: KSPLogger, 17 | ) : SymbolProcessor { 18 | 19 | private var targetList = mutableListOf() 20 | 21 | override fun process(resolver: Resolver): List { 22 | targetList.addAll(resolver.findUsefulPreferences(logger)) 23 | return emptyList() 24 | } 25 | 26 | override fun finish() { 27 | targetList.generatePreferences( 28 | codeGenerator = codeGenerator, 29 | logger = logger, 30 | ) 31 | targetList.clear() 32 | } 33 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/EncryptedDataStorePreferencesProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class EncryptedDataStorePreferencesProcessorProvider : SymbolProcessorProvider { 8 | 9 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = 10 | EncryptedDataStorePreferencesProcessor( 11 | codeGenerator = environment.codeGenerator, 12 | logger = environment.logger, 13 | ) 14 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/DataStoreConst.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.asClassName 5 | import kotlinx.coroutines.flow.Flow 6 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 8 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 9 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 10 | 11 | internal object DataStoreConst { 12 | 13 | const val DEBUG = false 14 | 15 | val ANNOTATION_USEFUL_PREFERENCES = UsefulPreferences::class.asClassName() 16 | const val ANNOTATION_DISABLE_SECURE = "disableSecure" 17 | 18 | val ANNOTATION_GET_VALUE = GetValue::class.asClassName() 19 | val ANNOTATION_SET_VALUE = SetValue::class.asClassName() 20 | const val ANNOTATION_KEY_ARGUMENT = "key" 21 | const val ANNOTATION_DEFAULT_ARGUMENT = "defaultValue" 22 | 23 | val ANNOTATION_CLEAR_VALUES = ClearValues::class.asClassName() 24 | 25 | val PREDEFINED_ANNOTATIONS = arrayOf( 26 | ANNOTATION_GET_VALUE.simpleName, ANNOTATION_SET_VALUE.simpleName, ANNOTATION_CLEAR_VALUES.simpleName, 27 | ) 28 | 29 | // data store preference 30 | val PREF_GENERATE_INT = ClassName("androidx.datastore.preferences.core", "intPreferencesKey") 31 | val PREF_GENERATE_DOUBLE = ClassName("androidx.datastore.preferences.core", "doublePreferencesKey") 32 | val PREF_GENERATE_STRING = ClassName("androidx.datastore.preferences.core", "stringPreferencesKey") 33 | val PREF_GENERATE_BOOLEAN = ClassName("androidx.datastore.preferences.core", "booleanPreferencesKey") 34 | val PREF_GENERATE_FLOAT = ClassName("androidx.datastore.preferences.core", "floatPreferencesKey") 35 | val PREF_GENERATE_LONG = ClassName("androidx.datastore.preferences.core", "longPreferencesKey") 36 | val PREF_PREFERENCES = ClassName("androidx.datastore.preferences.core", "Preferences") 37 | val PREF_PREFERENCES_KEY = ClassName("androidx.datastore.preferences.core", "Preferences.Key") 38 | val PREF_DATA_STORE = ClassName("androidx.datastore.core", "DataStore") 39 | val PREF_DATA_STORE_EDIT = ClassName("androidx.datastore.preferences.core", "edit") 40 | const val PREF_IMPL_PRIMARY_PROPERTY = "preferencesStore" 41 | 42 | // Coroutine 43 | val FLOW = Flow::class.asClassName() 44 | val FLOW_MAP = ClassName("kotlinx.coroutines.flow", "map") 45 | val FLOW_FIRST = ClassName("kotlinx.coroutines.flow", "first") 46 | 47 | // security 48 | const val USEFUL_SECURITY_PRIMARY_PROPERTY = "usefulSecurity" 49 | val USEFUL_SECURITY = ClassName("tech.thdev.useful.encrypted.data.store.preferences.security", "UsefulSecurity") 50 | val EDIT_ENCRYPT = ClassName("tech.thdev.useful.encrypted.data.store.preferences.extensions", "editEncrypt") 51 | val FLOW_MAP_DECRYPT = ClassName("tech.thdev.useful.encrypted.data.store.preferences.extensions", "mapDecrypt") 52 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/StringUtil.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal 2 | 3 | internal fun String.mergePrefixGenerate(): String = 4 | "generate$this" 5 | 6 | internal fun String.upperKey(): String = 7 | replace("-", "_").uppercase() 8 | 9 | /** 10 | * Default value 11 | */ 12 | internal fun String.generateDefaultValue(defaultValue: String): String = when (this) { 13 | Int::class.simpleName -> "${defaultValue.toIntOrNull() ?: "0"}" 14 | Double::class.simpleName -> "${defaultValue.toDoubleOrNull() ?: "0.0"}" 15 | String::class.simpleName -> "\"$defaultValue\"" 16 | Boolean::class.simpleName -> "${defaultValue.toBooleanStrictOrNull() ?: "false"}" 17 | Float::class.simpleName -> "${defaultValue.toFloatOrNull() ?: "0.0F"}" 18 | Long::class.simpleName -> "${defaultValue.toLongOrNull() ?: "0L"}" 19 | else -> { 20 | throw Exception("Useful Preference generate is Not support type $this") 21 | } 22 | } 23 | 24 | /** 25 | * Default value 26 | */ 27 | internal fun String.generateDefaultValueStringType(defaultValue: String): String = when (this) { 28 | Int::class.simpleName -> "\"${defaultValue.toIntOrNull() ?: "0"}\"" 29 | Double::class.simpleName -> "\"${defaultValue.toDoubleOrNull() ?: "0.0"}\"" 30 | String::class.simpleName -> "\"$defaultValue\"" 31 | Boolean::class.simpleName -> "\"${defaultValue.toBooleanStrictOrNull() ?: "false"}\"" 32 | Float::class.simpleName -> "\"${defaultValue.toFloatOrNull() ?: "0.0F"}\"" 33 | Long::class.simpleName -> "\"${defaultValue.toLongOrNull() ?: "0"}\"" 34 | else -> { 35 | throw Exception("Useful Preference generate is Not support type $this") 36 | } 37 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/Util.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal 2 | 3 | import com.google.devtools.ksp.processing.CodeGenerator 4 | import com.google.devtools.ksp.processing.Dependencies 5 | import com.google.devtools.ksp.processing.KSPLogger 6 | import com.google.devtools.ksp.processing.Resolver 7 | import com.google.devtools.ksp.symbol.KSClassDeclaration 8 | import com.google.devtools.ksp.symbol.KSDeclaration 9 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 10 | import com.google.devtools.ksp.symbol.KSName 11 | import com.google.devtools.ksp.symbol.Modifier 12 | import com.squareup.kotlinpoet.FileSpec 13 | import java.io.OutputStreamWriter 14 | 15 | internal fun KSFunctionDeclaration.filterAnnotation(predicate: (name: String) -> Boolean): Boolean = 16 | annotations.firstOrNull { ksAnnotated -> 17 | predicate(ksAnnotated.shortName.asString()) 18 | } != null 19 | 20 | /** 21 | * find `key` argument 22 | */ 23 | internal fun KSFunctionDeclaration.findArgument(key: String): String? = 24 | annotations.firstOrNull()?.arguments?.firstOrNull { ksValueArgument -> 25 | ksValueArgument.name?.asString() == key 26 | }?.let { ksValueArgument -> 27 | ksValueArgument.value as String 28 | } 29 | 30 | /** 31 | * find `key` argument 32 | */ 33 | internal fun KSClassDeclaration.findDisableEncrypted(): Boolean = 34 | annotations.firstOrNull()?.arguments?.firstOrNull { ksValueArgument -> 35 | ksValueArgument.name?.asString() == DataStoreConst.ANNOTATION_DISABLE_SECURE 36 | }?.let { ksValueArgument -> 37 | ksValueArgument.value as Boolean 38 | } ?: false 39 | 40 | internal fun Set.hasSuspend(): Boolean = 41 | firstOrNull { it == Modifier.SUSPEND } != null 42 | 43 | internal fun KSPLogger.writeLogger(message: String) { 44 | if (DataStoreConst.DEBUG) { 45 | warn(message) 46 | } 47 | } 48 | 49 | internal fun KSFunctionDeclaration.getReturnElement(): KSDeclaration? = 50 | returnType?.element?.typeArguments?.firstOrNull()?.type?.resolve()?.declaration 51 | 52 | internal fun KSFunctionDeclaration.getReturnResolve(): KSDeclaration? = 53 | returnType?.resolve()?.declaration 54 | 55 | internal fun KSFunctionDeclaration.hasFlow(): Boolean = 56 | getReturnResolve()?.simpleName?.asString() == DataStoreConst.FLOW.simpleName 57 | 58 | internal fun FileSpec.writeTo( 59 | codeGenerator: CodeGenerator, 60 | packageName: String, 61 | fileName: String, 62 | ) { 63 | val outputStream = codeGenerator.createNewFile( 64 | Dependencies.ALL_FILES, 65 | packageName, 66 | fileName 67 | ) 68 | 69 | OutputStreamWriter(outputStream, "UTF-8").use { writeTo(it) } 70 | } 71 | 72 | /** 73 | * make sure it's accessible 74 | */ 75 | internal fun Resolver.findClassDeclaration( 76 | packageName: String, 77 | className: String, 78 | ): KSClassDeclaration? = 79 | getClassDeclarationByName(object : KSName { 80 | override fun asString(): String = 81 | "$packageName.$className" 82 | 83 | override fun getQualifier(): String = 84 | packageName 85 | 86 | override fun getShortName(): String = 87 | className 88 | }) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/generate/GeneratePreferences.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate 2 | 3 | import com.google.devtools.ksp.processing.CodeGenerator 4 | import com.google.devtools.ksp.processing.KSPLogger 5 | import com.google.devtools.ksp.symbol.KSName 6 | import com.squareup.kotlinpoet.ClassName 7 | import com.squareup.kotlinpoet.FileSpec 8 | import com.squareup.kotlinpoet.FunSpec 9 | import com.squareup.kotlinpoet.KModifier 10 | import com.squareup.kotlinpoet.ParameterSpec 11 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 12 | import com.squareup.kotlinpoet.PropertySpec 13 | import com.squareup.kotlinpoet.TypeSpec 14 | import com.squareup.kotlinpoet.TypeVariableName 15 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 16 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function.generateClearFunction 17 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function.generateGetFunction 18 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function.generateSetFunction 19 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.mergePrefixGenerate 20 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.DataType 21 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.ResearchModel 22 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.upperKey 23 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.writeTo 24 | 25 | internal class GeneratePreferences( 26 | private val codeGenerator: CodeGenerator, 27 | @Suppress("unused") private val logger: KSPLogger, 28 | ) { 29 | 30 | fun generate(researchModels: List) { 31 | researchModels.forEach { item -> 32 | val className = item.targetClassDeclaration.simpleName.getShortName() 33 | val packageName = item.targetClassDeclaration.packageName.asString() 34 | 35 | val keyClassName = item.mergeKeyModel.generateKeysClass(item.disableSecurity, className, packageName) 36 | generatePreferenceImplClass(className, packageName, keyClassName, item) 37 | } 38 | } 39 | 40 | /** 41 | * Generate key class 42 | * 43 | * DataStore key class 44 | */ 45 | private fun Map.generateKeysClass( 46 | disableSecurity: Boolean, 47 | className: String, 48 | packageName: String, 49 | ): ClassName { 50 | val newClassName = "${className}Keys" 51 | val fileSpec = FileSpec.builder(packageName = packageName, fileName = newClassName) 52 | .addImport(DataStoreConst.PREF_PREFERENCES.packageName, DataStoreConst.PREF_PREFERENCES.simpleName) 53 | .addImport(DataStoreConst.PREF_PREFERENCES_KEY.packageName, DataStoreConst.PREF_PREFERENCES_KEY.simpleName) 54 | 55 | val classSpec = TypeSpec.objectBuilder(name = newClassName) 56 | .addModifiers(KModifier.INTERNAL) 57 | forEach { (key, type) -> 58 | val propertySpec = if (disableSecurity) { 59 | when (type.asString()) { 60 | Int::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_INT, fileSpec) 61 | Double::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_DOUBLE, fileSpec) 62 | String::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_STRING, fileSpec) 63 | Boolean::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_BOOLEAN, fileSpec) 64 | Float::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_FLOAT, fileSpec) 65 | Long::class.qualifiedName -> key.createProperty(type.asString(), DataStoreConst.PREF_GENERATE_LONG, fileSpec) 66 | else -> { 67 | throw Exception("Useful Preference generate is Not support type ${type.asString()}, ${String::class.simpleName}") 68 | } 69 | } 70 | } else { 71 | key.createProperty(String::class.qualifiedName ?: "", DataStoreConst.PREF_GENERATE_STRING, fileSpec) 72 | } 73 | classSpec.addProperty(propertySpec.build()) 74 | } 75 | 76 | fileSpec.addType(classSpec.build()) 77 | 78 | // Write file 79 | fileSpec.build().writeTo( 80 | codeGenerator = codeGenerator, 81 | packageName = packageName, 82 | fileName = newClassName, 83 | ) 84 | 85 | return ClassName(packageName = packageName, newClassName) 86 | } 87 | 88 | private fun String.createProperty(type: String, typeBuilder: ClassName, fileSpec: FileSpec.Builder): PropertySpec.Builder { 89 | fileSpec.addImport(typeBuilder.packageName, typeBuilder.simpleName) 90 | 91 | return PropertySpec 92 | .builder( 93 | name = upperKey(), 94 | type = DataStoreConst.PREF_PREFERENCES_KEY.parameterizedBy(TypeVariableName(type)), 95 | ) 96 | .initializer("${typeBuilder.simpleName}(\"$this\")") 97 | } 98 | 99 | /** 100 | * internal class XXXImpl( 101 | private val preferencesStore: DataStore 102 | ) : XXX { 103 | 104 | override fun getXXX(): Flow = 105 | userPreferencesStore.data 106 | .map { 107 | it[`UserPreferencesKeys.KEY_XXX] ?: "" 108 | } 109 | 110 | override suspend fun setXxx(xxx: String) { 111 | userPreferencesStore.edit { 112 | it[`UserPreferencesKeys.KEY_XXX] = xxx 113 | } 114 | } 115 | } 116 | */ 117 | private fun generatePreferenceImplClass( 118 | className: String, 119 | packageName: String, 120 | keyClassName: ClassName, 121 | researchModel: ResearchModel, 122 | ) { 123 | val superClass = TypeVariableName(className) 124 | val newClassName = "${className}Impl" 125 | val fileSpec = FileSpec.builder(packageName = packageName, fileName = newClassName) 126 | 127 | val classSpec = TypeSpec.classBuilder(name = newClassName) 128 | .addModifiers(KModifier.INTERNAL) 129 | .addSuperinterface(superClass) 130 | 131 | val primaryPreferencesStore = DataStoreConst.PREF_IMPL_PRIMARY_PROPERTY 132 | val primaryPreferencesStoreType = DataStoreConst.PREF_DATA_STORE.parameterizedBy(DataStoreConst.PREF_PREFERENCES) 133 | 134 | if (researchModel.disableSecurity) { 135 | classSpec.primaryConstructor( 136 | FunSpec.constructorBuilder() 137 | .addParameter(primaryPreferencesStore, primaryPreferencesStoreType) 138 | .build() 139 | ) 140 | .addProperty( 141 | PropertySpec.builder(primaryPreferencesStore, primaryPreferencesStoreType) 142 | .initializer(primaryPreferencesStore) 143 | .addModifiers(KModifier.PRIVATE) 144 | .build() 145 | ) 146 | } else { 147 | val primarySecurity = DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY 148 | val primarySecurityType = DataStoreConst.USEFUL_SECURITY 149 | classSpec.primaryConstructor( 150 | FunSpec.constructorBuilder() 151 | .addParameter(primarySecurity, primarySecurityType) 152 | .addParameter(primaryPreferencesStore, primaryPreferencesStoreType) 153 | .build() 154 | ) 155 | .addProperty( 156 | PropertySpec.builder(primarySecurity, primarySecurityType) 157 | .initializer(primarySecurity) 158 | .addModifiers(KModifier.PRIVATE) 159 | .build() 160 | ) 161 | .addProperty( 162 | PropertySpec.builder(primaryPreferencesStore, primaryPreferencesStoreType) 163 | .initializer(primaryPreferencesStore) 164 | .addModifiers(KModifier.PRIVATE) 165 | .build() 166 | ) 167 | } 168 | 169 | // Generate functions. 170 | researchModel.dataTypes.forEach { item -> 171 | val funSpec = when (item) { 172 | is DataType.Get -> { 173 | // Generate get function 174 | item.generateGetFunction(researchModel.disableSecurity, fileSpec, primaryPreferencesStore, keyClassName.simpleName) 175 | } 176 | is DataType.Set -> { 177 | // Generate set function 178 | item.generateSetFunction(researchModel.disableSecurity, fileSpec, primaryPreferencesStore, keyClassName.simpleName) 179 | } 180 | is DataType.Clear -> { 181 | // Generate clear 182 | item.generateClearFunction(fileSpec, primaryPreferencesStore) 183 | } 184 | } 185 | classSpec.addFunction(funSpec.build()) 186 | } 187 | 188 | fileSpec.addType(classSpec.build()) 189 | 190 | // generate function 191 | // DataStore.generateXXX(): XXX = XXXImpl(this) 192 | if (researchModel.disableSecurity) { 193 | val funSpec = FunSpec.builder(className.mergePrefixGenerate()) 194 | .returns(superClass) 195 | .receiver(primaryPreferencesStoreType) 196 | .addStatement("return %N(%N = this)", newClassName, DataStoreConst.PREF_IMPL_PRIMARY_PROPERTY) 197 | fileSpec.addFunction(funSpec.build()) 198 | } else { 199 | val funSpec = FunSpec.builder(className.mergePrefixGenerate()) 200 | .returns(superClass) 201 | .receiver(primaryPreferencesStoreType) 202 | .addParameter(ParameterSpec.builder(DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY, DataStoreConst.USEFUL_SECURITY).build()) 203 | .addStatement( 204 | "return %N(%N = this, %N = %N)", 205 | newClassName, 206 | DataStoreConst.PREF_IMPL_PRIMARY_PROPERTY, 207 | DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY, 208 | DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY, 209 | ) 210 | fileSpec.addFunction(funSpec.build()) 211 | } 212 | 213 | fileSpec.build().writeTo( 214 | codeGenerator = codeGenerator, 215 | packageName = packageName, 216 | fileName = newClassName, 217 | ) 218 | } 219 | } 220 | 221 | internal fun List.generatePreferences( 222 | codeGenerator: CodeGenerator, 223 | logger: KSPLogger, 224 | ) = 225 | GeneratePreferences( 226 | logger = logger, 227 | codeGenerator = codeGenerator, 228 | ).generate(this) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/generate/function/GenerateClearFunction.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | import com.squareup.kotlinpoet.FunSpec 5 | import com.squareup.kotlinpoet.KModifier 6 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.DataType 8 | 9 | internal fun DataType.Clear.generateClearFunction( 10 | fileSpec: FileSpec.Builder, 11 | primaryContractValue: String, 12 | ): FunSpec.Builder { 13 | val funSpec = FunSpec.builder(functionInfo.simpleName.asString()) 14 | .addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC) 15 | if (isSuspend) { 16 | funSpec.addModifiers(KModifier.SUSPEND) 17 | } 18 | 19 | // generate body 20 | fileSpec.addImport(DataStoreConst.PREF_DATA_STORE_EDIT.packageName, DataStoreConst.PREF_DATA_STORE_EDIT.simpleName) 21 | funSpec.addStatement( 22 | "$primaryContractValue.${DataStoreConst.PREF_DATA_STORE_EDIT.simpleName}" + 23 | " {\nit.clear()\n}" 24 | ) 25 | return funSpec 26 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/generate/function/GenerateGetFunction.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.FileSpec 5 | import com.squareup.kotlinpoet.FunSpec 6 | import com.squareup.kotlinpoet.KModifier 7 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 8 | import com.squareup.kotlinpoet.TypeVariableName 9 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 10 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generateDefaultValue 11 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generateDefaultValueStringType 12 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.getReturnElement 13 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.getReturnResolve 14 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.hasFlow 15 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.DataType 16 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.upperKey 17 | 18 | /** 19 | * Generate getFunction 20 | * 21 | override fun getXXX(): Flow = 22 | userPreferencesStore.data 23 | .map { 24 | it[`XXXKeys.KEY_XXX] ?: `defaultValue` 25 | } 26 | */ 27 | internal fun DataType.Get.generateGetFunction( 28 | disableSecurity: Boolean, 29 | fileSpec: FileSpec.Builder, 30 | primaryContractValue: String, 31 | keyClassName: String, 32 | ): FunSpec.Builder { 33 | val funSpec = FunSpec.builder(functionInfo.simpleName.asString()) 34 | .addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC) 35 | 36 | fun String.getMap() = 37 | "return $primaryContractValue.data" + 38 | "\n.map {" + 39 | "\nit[$keyClassName.${key.upperKey()}] ?: ${this.generateDefaultValue(defaultValue)}" + 40 | "\n}" 41 | 42 | fun String.getMapDecrypt() = 43 | "return $primaryContractValue.data" + 44 | "\n.mapDecrypt<$this>(" + 45 | "\n\tusefulSecurity = ${DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY}," + 46 | "\n\ttype = $this::class," + 47 | "\n\tdefaultValue = ${this.generateDefaultValueStringType(defaultValue)}," + 48 | "\n) {" + 49 | "\nit[$keyClassName.${key.upperKey()}]" + 50 | "\n}" 51 | 52 | if (isSuspend) { // flow to value 53 | fileSpec.addImport(DataStoreConst.FLOW_FIRST.packageName, DataStoreConst.FLOW_FIRST.simpleName) 54 | 55 | funSpec.addModifiers(KModifier.SUSPEND) 56 | .returns(TypeVariableName(valueType.getShortName())) 57 | 58 | if (disableSecurity) { 59 | fileSpec.addImport(DataStoreConst.FLOW_MAP.packageName, DataStoreConst.FLOW_MAP.simpleName) 60 | funSpec.addCode("${valueType.getShortName().getMap()}\n.first()") 61 | } else { 62 | fileSpec.addImport(DataStoreConst.FLOW_MAP_DECRYPT.packageName, DataStoreConst.FLOW_MAP_DECRYPT.simpleName) 63 | funSpec.addCode("${valueType.getShortName().getMapDecrypt()}\n.first()") 64 | } 65 | } else if (functionInfo.hasFlow()) { // flow return 66 | // find generic type 67 | functionInfo.getReturnResolve()?.typeParameters?.firstOrNull()?.let { _ -> 68 | // get full name 69 | if (functionInfo.getReturnResolve()?.qualifiedName != null && functionInfo.getReturnElement()?.simpleName != null) { 70 | val genericName = functionInfo.getReturnResolve()!!.qualifiedName!! 71 | val typeName = functionInfo.getReturnElement()!!.simpleName.getShortName() 72 | val genericClassName = ClassName(packageName = genericName.getQualifier(), genericName.getShortName()) 73 | funSpec.returns(genericClassName.parameterizedBy(TypeVariableName(typeName))) 74 | 75 | if (disableSecurity) { 76 | fileSpec.addImport(DataStoreConst.FLOW_MAP.packageName, DataStoreConst.FLOW_MAP.simpleName) 77 | funSpec.addCode(typeName.getMap()) 78 | } else { 79 | fileSpec.addImport(DataStoreConst.FLOW_MAP_DECRYPT.packageName, DataStoreConst.FLOW_MAP_DECRYPT.simpleName) 80 | funSpec.addCode(typeName.getMapDecrypt()) 81 | } 82 | } 83 | } 84 | } else { 85 | throw Exception("${functionInfo.simpleName.getShortName()} Not support type") 86 | } 87 | 88 | return funSpec 89 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/generate/function/GenerateSetFunction.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.generate.function 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | import com.squareup.kotlinpoet.FunSpec 5 | import com.squareup.kotlinpoet.KModifier 6 | import com.squareup.kotlinpoet.TypeVariableName 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 8 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.DataType 9 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.upperKey 10 | 11 | /** 12 | * Generate set function. 13 | * 14 | public override suspend fun setXXX(`value`: Type): Unit { 15 | preferencesStore.edit { 16 | it[`SecurityPreferencesKeys.KEY_DOUBLE] = value 17 | } 18 | } 19 | */ 20 | internal fun DataType.Set.generateSetFunction( 21 | disableSecurity: Boolean, 22 | fileSpec: FileSpec.Builder, 23 | primaryContractValue: String, 24 | keyClassName: String, 25 | ): FunSpec.Builder { 26 | val funSpec = FunSpec.builder(functionInfo.simpleName.asString()) 27 | .addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC) 28 | if (isSuspend) { 29 | funSpec.addModifiers(KModifier.SUSPEND) 30 | } 31 | 32 | functionInfo.parameters.firstOrNull().takeIf { it?.name?.asString() != null }?.let { parameter -> 33 | val parameterName = parameter.name!!.asString() 34 | funSpec.addParameter(parameterName, TypeVariableName(parameter.type.resolve().declaration.simpleName.asString())) 35 | 36 | // generate body 37 | if (disableSecurity) { 38 | fileSpec.addImport(DataStoreConst.PREF_DATA_STORE_EDIT.packageName, DataStoreConst.PREF_DATA_STORE_EDIT.simpleName) 39 | funSpec.addStatement( 40 | "$primaryContractValue.${DataStoreConst.PREF_DATA_STORE_EDIT.simpleName}" + 41 | " {\nit[$keyClassName.${key.upperKey()}] = $parameterName\n}" 42 | ) 43 | } else { 44 | fileSpec.addImport(DataStoreConst.EDIT_ENCRYPT.packageName, DataStoreConst.EDIT_ENCRYPT.simpleName) 45 | funSpec.addStatement( 46 | "$primaryContractValue.${DataStoreConst.EDIT_ENCRYPT.simpleName}" + 47 | "(${DataStoreConst.USEFUL_SECURITY_PRIMARY_PROPERTY}, $parameterName) { preferences, encrypted ->" + 48 | " \npreferences[$keyClassName.${key.upperKey()}] = encrypted\n}" 49 | ) 50 | } 51 | } 52 | return funSpec 53 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/model/DataType.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model 2 | 3 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 4 | import com.google.devtools.ksp.symbol.KSName 5 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.hasSuspend 6 | 7 | sealed interface DataType { 8 | 9 | val functionInfo: KSFunctionDeclaration 10 | val isSuspend: Boolean 11 | 12 | interface KeyValue { 13 | val key: String 14 | val valueType: KSName 15 | } 16 | 17 | data class Set( 18 | override val key: String, 19 | override val functionInfo: KSFunctionDeclaration, 20 | override val valueType: KSName, 21 | override val isSuspend: Boolean = functionInfo.modifiers.hasSuspend(), 22 | ) : DataType, KeyValue 23 | 24 | data class Get( 25 | override val key: String, 26 | val defaultValue: String, 27 | override val functionInfo: KSFunctionDeclaration, 28 | override val valueType: KSName, 29 | override val isSuspend: Boolean = functionInfo.modifiers.hasSuspend(), 30 | ) : DataType, KeyValue 31 | 32 | data class Clear( 33 | override val functionInfo: KSFunctionDeclaration, 34 | override val isSuspend: Boolean = functionInfo.modifiers.hasSuspend(), 35 | ) : DataType 36 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/model/ResearchModel.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model 2 | 3 | import com.google.devtools.ksp.symbol.KSClassDeclaration 4 | import com.google.devtools.ksp.symbol.KSName 5 | 6 | data class ResearchModel( 7 | val disableSecurity: Boolean, 8 | val targetClassDeclaration: KSClassDeclaration, 9 | val dataTypes: List, 10 | val mergeKeyModel: Map, 11 | ) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/internal/visitor/FindDataStorePreferences.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.visitor 2 | 3 | import com.google.devtools.ksp.getDeclaredFunctions 4 | import com.google.devtools.ksp.processing.KSPLogger 5 | import com.google.devtools.ksp.processing.Resolver 6 | import com.google.devtools.ksp.symbol.KSClassDeclaration 7 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.DataStoreConst 8 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.filterAnnotation 9 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.findArgument 10 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.findClassDeclaration 11 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.findDisableEncrypted 12 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.getReturnElement 13 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.getReturnResolve 14 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.hasSuspend 15 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.DataType 16 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.model.ResearchModel 17 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.internal.writeLogger 18 | 19 | /** 20 | * Research Annotation and struct. 21 | */ 22 | internal fun Resolver.findUsefulPreferences( 23 | @Suppress("UNUSED_PARAMETER") logger: KSPLogger, 24 | ): List = 25 | getSymbolsWithAnnotation(DataStoreConst.ANNOTATION_USEFUL_PREFERENCES.canonicalName) 26 | .filter { ksAnnotated -> 27 | ksAnnotated is KSClassDeclaration 28 | } 29 | .map { ksAnnotated -> 30 | ksAnnotated as KSClassDeclaration 31 | } 32 | .map { classDeclaration -> 33 | val declaredFunctions = classDeclaration.getDeclaredFunctions() 34 | val disableSecurity = classDeclaration.findDisableEncrypted() 35 | 36 | val findDeclaredFunctions = declaredFunctions 37 | .filter { functionDeclaration -> 38 | functionDeclaration.filterAnnotation { 39 | DataStoreConst.PREDEFINED_ANNOTATIONS.contains(it) 40 | } 41 | } 42 | .mapNotNull { functionDeclaration -> 43 | when (functionDeclaration.annotations.first().shortName.asString()) { 44 | DataStoreConst.ANNOTATION_GET_VALUE.simpleName -> { 45 | // First, the return type is found in the element. 46 | // If there is no information, a non generic type is searched through resolve(). 47 | (functionDeclaration.getReturnElement()?.simpleName 48 | ?: functionDeclaration.getReturnResolve()?.simpleName)?.let { returnType -> 49 | functionDeclaration.findArgument(DataStoreConst.ANNOTATION_KEY_ARGUMENT)?.let { key -> 50 | DataType.Get( 51 | key = key, 52 | defaultValue = functionDeclaration.findArgument(DataStoreConst.ANNOTATION_DEFAULT_ARGUMENT) ?: "", 53 | functionInfo = functionDeclaration, 54 | valueType = returnType, 55 | isSuspend = functionDeclaration.modifiers.hasSuspend(), 56 | ) 57 | } 58 | } 59 | } 60 | DataStoreConst.ANNOTATION_SET_VALUE.simpleName -> { 61 | functionDeclaration.parameters.firstOrNull()?.type?.resolve()?.declaration?.qualifiedName?.let { parameters -> 62 | functionDeclaration.findArgument(DataStoreConst.ANNOTATION_KEY_ARGUMENT)?.let { key -> 63 | DataType.Set( 64 | key = key, 65 | functionInfo = functionDeclaration, 66 | isSuspend = functionDeclaration.modifiers.hasSuspend(), 67 | valueType = parameters, 68 | ) 69 | } 70 | } 71 | } 72 | DataStoreConst.ANNOTATION_CLEAR_VALUES.simpleName -> { 73 | DataType.Clear( 74 | functionInfo = functionDeclaration, 75 | isSuspend = functionDeclaration.modifiers.hasSuspend(), 76 | ) 77 | } 78 | else -> null 79 | } 80 | } 81 | .onEach { dataType -> 82 | if (dataType is DataType.KeyValue) { 83 | logger.writeLogger("------ Find new key info : ${dataType.key}") 84 | } 85 | logger.writeLogger("function name : ${dataType.functionInfo.simpleName.asString()}") 86 | dataType.functionInfo.modifiers.forEach { modifier -> 87 | logger.writeLogger("function modifier : ${modifier.name}") 88 | } 89 | logger.writeLogger("return type default : ${dataType.functionInfo.getReturnResolve()?.simpleName?.asString()}") 90 | logger.writeLogger("return type full name : ${dataType.functionInfo.getReturnResolve()?.qualifiedName?.asString()}") 91 | logger.writeLogger("return type generic : ${dataType.functionInfo.getReturnResolve()?.typeParameters?.lastOrNull()}") 92 | logger.writeLogger("return type generic type name : ${dataType.functionInfo.getReturnElement()?.simpleName?.asString()}") 93 | logger.writeLogger("parameter name : ${dataType.functionInfo.parameters.firstOrNull()}") 94 | logger.writeLogger("parameter type : ${dataType.functionInfo.parameters.firstOrNull()?.type?.resolve()?.declaration?.simpleName?.asString()}\n") 95 | } 96 | .toList() 97 | 98 | // Check enable security module 99 | if (disableSecurity.not()) { 100 | val findClass = this.findClassDeclaration(DataStoreConst.USEFUL_SECURITY.packageName, DataStoreConst.USEFUL_SECURITY.simpleName) 101 | if (findClass == null) { 102 | logger.error("Not found Useful security. add security dependency.") 103 | } 104 | } 105 | 106 | // list merge 107 | val mergeMap = findDeclaredFunctions 108 | .filter { dataType -> 109 | dataType is DataType.KeyValue 110 | } 111 | .map { dataType -> 112 | dataType as DataType.KeyValue 113 | } 114 | .associateBy( 115 | keySelector = { it.key }, 116 | valueTransform = { it.valueType } 117 | ) 118 | 119 | ResearchModel( 120 | disableSecurity = disableSecurity, 121 | targetClassDeclaration = classDeclaration, 122 | dataTypes = findDeclaredFunctions, 123 | mergeKeyModel = mergeMap 124 | ) 125 | } 126 | .toList() -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | tech.thdev.useful.encrypted.data.store.preferences.ksp.EncryptedDataStorePreferencesProcessorProvider -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-ksp/src/test/java/tech/thdev/useful/encrypted/data/store/preferences/ksp/EncryptedDataStorePreferencesProcessorProviderTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.ksp 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import com.tschuchort.compiletesting.symbolProcessorProviders 6 | import java.io.File 7 | import java.nio.file.Path 8 | import org.junit.jupiter.api.Assertions 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.io.TempDir 11 | 12 | internal class EncryptedDataStorePreferencesProcessorProviderTest { 13 | 14 | @Test 15 | fun `test NonSecurePreferences`() { 16 | val securityClass = SourceFile.kotlin( 17 | "UsefulSecurity.kt", 18 | """ 19 | package tech.thdev.useful.encrypted.data.store.preferences.security 20 | 21 | interface UsefulSecurity { 22 | 23 | /** 24 | * Data to encryptData 25 | */ 26 | fun encryptData(data: String): String 27 | 28 | /** 29 | * encryptData to decrypt 30 | */ 31 | fun decryptData(encryptData: String): String 32 | } 33 | """.trimIndent() 34 | ) 35 | 36 | val securityPreferences = SourceFile.kotlin( 37 | "SecurityPreferences.kt", 38 | """ 39 | package tech.thdev.samplepreference.repository 40 | 41 | import kotlinx.coroutines.flow.Flow 42 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 43 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 44 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 45 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 46 | 47 | @UsefulPreferences(disableSecure = true) 48 | interface SecurityPreferences { 49 | 50 | @GetValue(KEY_INT) 51 | fun getInt(): Flow 52 | 53 | @GetValue(KEY_INT) 54 | suspend fun getIntValue(): Int 55 | 56 | @SetValue(KEY_INT) 57 | suspend fun setInt(value: Int) 58 | 59 | @GetValue(KEY_DOUBLE) 60 | fun getDouble(): Flow 61 | 62 | @SetValue(KEY_DOUBLE) 63 | suspend fun setDouble(value: Double) 64 | 65 | @GetValue(KEY_STRING) 66 | fun getString(): Flow 67 | 68 | @SetValue(KEY_STRING) 69 | suspend fun setString(value: String) 70 | 71 | @GetValue(KEY_BOOLEAN) 72 | fun getBoolean(): Flow 73 | 74 | @SetValue(KEY_BOOLEAN) 75 | suspend fun setBoolean(value: Boolean) 76 | 77 | @GetValue(KEY_FLOAT) 78 | fun getFloat(): Flow 79 | 80 | @SetValue(KEY_FLOAT) 81 | suspend fun setFloat(value: Float) 82 | 83 | @GetValue(KEY_LONG) 84 | fun getLong(): Flow 85 | 86 | @SetValue(KEY_LONG) 87 | suspend fun setLong(value: Long) 88 | 89 | @ClearValues 90 | suspend fun clearAll() 91 | 92 | companion object { 93 | 94 | private const val KEY_INT = "key-int" 95 | private const val KEY_DOUBLE = "key-double" 96 | private const val KEY_STRING = "key-string" 97 | private const val KEY_BOOLEAN = "key-boolean" 98 | private const val KEY_FLOAT = "key-float" 99 | private const val KEY_LONG = "key-long" 100 | } 101 | } 102 | """.trimIndent() 103 | ) 104 | 105 | val compilationResult = compile(securityClass, securityPreferences) 106 | 107 | // SecurityPreferencesKeys 108 | Assertions.assertEquals( 109 | """ 110 | package tech.thdev.samplepreference.repository 111 | 112 | import androidx.datastore.preferences.core.Preferences 113 | import androidx.datastore.preferences.core.Preferences.Key 114 | import androidx.datastore.preferences.core.booleanPreferencesKey 115 | import androidx.datastore.preferences.core.doublePreferencesKey 116 | import androidx.datastore.preferences.core.floatPreferencesKey 117 | import androidx.datastore.preferences.core.intPreferencesKey 118 | import androidx.datastore.preferences.core.longPreferencesKey 119 | import androidx.datastore.preferences.core.stringPreferencesKey 120 | 121 | internal object SecurityPreferencesKeys { 122 | public val KEY_INT: Preferences.Key = intPreferencesKey("key-int") 123 | 124 | public val KEY_DOUBLE: Preferences.Key = doublePreferencesKey("key-double") 125 | 126 | public val KEY_STRING: Preferences.Key = stringPreferencesKey("key-string") 127 | 128 | public val KEY_BOOLEAN: Preferences.Key = booleanPreferencesKey("key-boolean") 129 | 130 | public val KEY_FLOAT: Preferences.Key = floatPreferencesKey("key-float") 131 | 132 | public val KEY_LONG: Preferences.Key = longPreferencesKey("key-long") 133 | } 134 | """.trimIndent(), 135 | compilationResult.sourceFor("SecurityPreferencesKeys.kt").trimIndent().replaceTab() 136 | ) 137 | 138 | // SecurityPreferencesImpl 139 | Assertions.assertEquals( 140 | """ 141 | package tech.thdev.samplepreference.repository 142 | 143 | import androidx.datastore.core.DataStore 144 | import androidx.datastore.preferences.core.Preferences 145 | import androidx.datastore.preferences.core.edit 146 | import kotlinx.coroutines.flow.Flow 147 | import kotlinx.coroutines.flow.first 148 | import kotlinx.coroutines.flow.map 149 | 150 | internal class SecurityPreferencesImpl( 151 | private val preferencesStore: DataStore, 152 | ) : SecurityPreferences { 153 | public override fun getInt(): Flow = preferencesStore.data 154 | .map { 155 | it[SecurityPreferencesKeys.KEY_INT] ?: 0 156 | } 157 | 158 | public override suspend fun getIntValue(): Int = preferencesStore.data 159 | .map { 160 | it[SecurityPreferencesKeys.KEY_INT] ?: 0 161 | } 162 | .first() 163 | 164 | public override suspend fun setInt(`value`: Int) { 165 | preferencesStore.edit { 166 | it[SecurityPreferencesKeys.KEY_INT] = value 167 | } 168 | } 169 | 170 | public override fun getDouble(): Flow = preferencesStore.data 171 | .map { 172 | it[SecurityPreferencesKeys.KEY_DOUBLE] ?: 0.0 173 | } 174 | 175 | public override suspend fun setDouble(`value`: Double) { 176 | preferencesStore.edit { 177 | it[SecurityPreferencesKeys.KEY_DOUBLE] = value 178 | } 179 | } 180 | 181 | public override fun getString(): Flow = preferencesStore.data 182 | .map { 183 | it[SecurityPreferencesKeys.KEY_STRING] ?: "" 184 | } 185 | 186 | public override suspend fun setString(`value`: String) { 187 | preferencesStore.edit { 188 | it[SecurityPreferencesKeys.KEY_STRING] = value 189 | } 190 | } 191 | 192 | public override fun getBoolean(): Flow = preferencesStore.data 193 | .map { 194 | it[SecurityPreferencesKeys.KEY_BOOLEAN] ?: false 195 | } 196 | 197 | public override suspend fun setBoolean(`value`: Boolean) { 198 | preferencesStore.edit { 199 | it[SecurityPreferencesKeys.KEY_BOOLEAN] = value 200 | } 201 | } 202 | 203 | public override fun getFloat(): Flow = preferencesStore.data 204 | .map { 205 | it[SecurityPreferencesKeys.KEY_FLOAT] ?: 0.0F 206 | } 207 | 208 | public override suspend fun setFloat(`value`: Float) { 209 | preferencesStore.edit { 210 | it[SecurityPreferencesKeys.KEY_FLOAT] = value 211 | } 212 | } 213 | 214 | public override fun getLong(): Flow = preferencesStore.data 215 | .map { 216 | it[SecurityPreferencesKeys.KEY_LONG] ?: 0L 217 | } 218 | 219 | public override suspend fun setLong(`value`: Long) { 220 | preferencesStore.edit { 221 | it[SecurityPreferencesKeys.KEY_LONG] = value 222 | } 223 | } 224 | 225 | public override suspend fun clearAll() { 226 | preferencesStore.edit { 227 | it.clear() 228 | } 229 | } 230 | } 231 | 232 | public fun DataStore.generateSecurityPreferences(): SecurityPreferences = 233 | SecurityPreferencesImpl(preferencesStore = this) 234 | """.trimIndent(), 235 | compilationResult.sourceFor("SecurityPreferencesImpl.kt").trimIndent().replaceTab() 236 | ) 237 | 238 | Assertions.assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode) 239 | } 240 | 241 | @Test 242 | fun `test NonSecurePreferences - default value`() { 243 | val securityClass = SourceFile.kotlin( 244 | "UsefulSecurity.kt", 245 | """ 246 | package tech.thdev.useful.encrypted.data.store.preferences.security 247 | 248 | interface UsefulSecurity { 249 | 250 | /** 251 | * Data to encryptData 252 | */ 253 | fun encryptData(data: String): String 254 | 255 | /** 256 | * encryptData to decrypt 257 | */ 258 | fun decryptData(encryptData: String): String 259 | } 260 | """.trimIndent() 261 | ) 262 | 263 | val securityPreferences = SourceFile.kotlin( 264 | "SecurityPreferences.kt", 265 | """ 266 | package tech.thdev.samplepreference.repository 267 | 268 | import kotlinx.coroutines.flow.Flow 269 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 270 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 271 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 272 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 273 | 274 | @UsefulPreferences(disableSecure = true) 275 | interface SecurityPreferences { 276 | 277 | @GetValue(KEY_INT, "30") 278 | fun getInt(): Flow 279 | 280 | @GetValue(KEY_INT, "3") 281 | suspend fun getIntValue(): Int 282 | 283 | @SetValue(KEY_INT) 284 | suspend fun setInt(value: Int) 285 | 286 | @GetValue(KEY_DOUBLE, "20.03") 287 | fun getDouble(): Flow 288 | 289 | @SetValue(KEY_DOUBLE) 290 | suspend fun setDouble(value: Double) 291 | 292 | @GetValue(KEY_STRING, "default value") 293 | fun getString(): Flow 294 | 295 | @SetValue(KEY_STRING) 296 | suspend fun setString(value: String) 297 | 298 | @GetValue(KEY_BOOLEAN, "true") 299 | fun getBoolean(): Flow 300 | 301 | @SetValue(KEY_BOOLEAN) 302 | suspend fun setBoolean(value: Boolean) 303 | 304 | @GetValue(KEY_FLOAT, "130.0") 305 | fun getFloat(): Flow 306 | 307 | @SetValue(KEY_FLOAT) 308 | suspend fun setFloat(value: Float) 309 | 310 | @GetValue(KEY_LONG, "60") 311 | fun getLong(): Flow 312 | 313 | @SetValue(KEY_LONG) 314 | suspend fun setLong(value: Long) 315 | 316 | @ClearValues 317 | suspend fun clearAll() 318 | 319 | companion object { 320 | 321 | private const val KEY_INT = "key-int" 322 | private const val KEY_DOUBLE = "key-double" 323 | private const val KEY_STRING = "key-string" 324 | private const val KEY_BOOLEAN = "key-boolean" 325 | private const val KEY_FLOAT = "key-float" 326 | private const val KEY_LONG = "key-long" 327 | } 328 | } 329 | """.trimIndent() 330 | ) 331 | 332 | val compilationResult = compile(securityClass, securityPreferences) 333 | 334 | // SecurityPreferencesKeys 335 | Assertions.assertEquals( 336 | """ 337 | package tech.thdev.samplepreference.repository 338 | 339 | import androidx.datastore.preferences.core.Preferences 340 | import androidx.datastore.preferences.core.Preferences.Key 341 | import androidx.datastore.preferences.core.booleanPreferencesKey 342 | import androidx.datastore.preferences.core.doublePreferencesKey 343 | import androidx.datastore.preferences.core.floatPreferencesKey 344 | import androidx.datastore.preferences.core.intPreferencesKey 345 | import androidx.datastore.preferences.core.longPreferencesKey 346 | import androidx.datastore.preferences.core.stringPreferencesKey 347 | 348 | internal object SecurityPreferencesKeys { 349 | public val KEY_INT: Preferences.Key = intPreferencesKey("key-int") 350 | 351 | public val KEY_DOUBLE: Preferences.Key = doublePreferencesKey("key-double") 352 | 353 | public val KEY_STRING: Preferences.Key = stringPreferencesKey("key-string") 354 | 355 | public val KEY_BOOLEAN: Preferences.Key = booleanPreferencesKey("key-boolean") 356 | 357 | public val KEY_FLOAT: Preferences.Key = floatPreferencesKey("key-float") 358 | 359 | public val KEY_LONG: Preferences.Key = longPreferencesKey("key-long") 360 | } 361 | """.trimIndent(), 362 | compilationResult.sourceFor("SecurityPreferencesKeys.kt").trimIndent().replaceTab() 363 | ) 364 | 365 | // SecurityPreferencesImpl 366 | Assertions.assertEquals( 367 | """ 368 | package tech.thdev.samplepreference.repository 369 | 370 | import androidx.datastore.core.DataStore 371 | import androidx.datastore.preferences.core.Preferences 372 | import androidx.datastore.preferences.core.edit 373 | import kotlinx.coroutines.flow.Flow 374 | import kotlinx.coroutines.flow.first 375 | import kotlinx.coroutines.flow.map 376 | 377 | internal class SecurityPreferencesImpl( 378 | private val preferencesStore: DataStore, 379 | ) : SecurityPreferences { 380 | public override fun getInt(): Flow = preferencesStore.data 381 | .map { 382 | it[SecurityPreferencesKeys.KEY_INT] ?: 30 383 | } 384 | 385 | public override suspend fun getIntValue(): Int = preferencesStore.data 386 | .map { 387 | it[SecurityPreferencesKeys.KEY_INT] ?: 3 388 | } 389 | .first() 390 | 391 | public override suspend fun setInt(`value`: Int) { 392 | preferencesStore.edit { 393 | it[SecurityPreferencesKeys.KEY_INT] = value 394 | } 395 | } 396 | 397 | public override fun getDouble(): Flow = preferencesStore.data 398 | .map { 399 | it[SecurityPreferencesKeys.KEY_DOUBLE] ?: 20.03 400 | } 401 | 402 | public override suspend fun setDouble(`value`: Double) { 403 | preferencesStore.edit { 404 | it[SecurityPreferencesKeys.KEY_DOUBLE] = value 405 | } 406 | } 407 | 408 | public override fun getString(): Flow = preferencesStore.data 409 | .map { 410 | it[SecurityPreferencesKeys.KEY_STRING] ?: "default value" 411 | } 412 | 413 | public override suspend fun setString(`value`: String) { 414 | preferencesStore.edit { 415 | it[SecurityPreferencesKeys.KEY_STRING] = value 416 | } 417 | } 418 | 419 | public override fun getBoolean(): Flow = preferencesStore.data 420 | .map { 421 | it[SecurityPreferencesKeys.KEY_BOOLEAN] ?: true 422 | } 423 | 424 | public override suspend fun setBoolean(`value`: Boolean) { 425 | preferencesStore.edit { 426 | it[SecurityPreferencesKeys.KEY_BOOLEAN] = value 427 | } 428 | } 429 | 430 | public override fun getFloat(): Flow = preferencesStore.data 431 | .map { 432 | it[SecurityPreferencesKeys.KEY_FLOAT] ?: 130.0 433 | } 434 | 435 | public override suspend fun setFloat(`value`: Float) { 436 | preferencesStore.edit { 437 | it[SecurityPreferencesKeys.KEY_FLOAT] = value 438 | } 439 | } 440 | 441 | public override fun getLong(): Flow = preferencesStore.data 442 | .map { 443 | it[SecurityPreferencesKeys.KEY_LONG] ?: 60 444 | } 445 | 446 | public override suspend fun setLong(`value`: Long) { 447 | preferencesStore.edit { 448 | it[SecurityPreferencesKeys.KEY_LONG] = value 449 | } 450 | } 451 | 452 | public override suspend fun clearAll() { 453 | preferencesStore.edit { 454 | it.clear() 455 | } 456 | } 457 | } 458 | 459 | public fun DataStore.generateSecurityPreferences(): SecurityPreferences = 460 | SecurityPreferencesImpl(preferencesStore = this) 461 | """.trimIndent(), 462 | compilationResult.sourceFor("SecurityPreferencesImpl.kt").trimIndent().replaceTab() 463 | ) 464 | 465 | Assertions.assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode) 466 | } 467 | 468 | @Test 469 | fun `test securityPreferences`() { 470 | val securityClass = SourceFile.kotlin( 471 | "UsefulSecurity.kt", 472 | """ 473 | package tech.thdev.useful.encrypted.data.store.preferences.security 474 | 475 | interface UsefulSecurity { 476 | 477 | /** 478 | * Data to encryptData 479 | */ 480 | fun encryptData(data: String): String 481 | 482 | /** 483 | * encryptData to decrypt 484 | */ 485 | fun decryptData(encryptData: String): String 486 | } 487 | """.trimIndent() 488 | ) 489 | 490 | val securityPreferences = SourceFile.kotlin( 491 | "SecurityPreferences.kt", 492 | """ 493 | package tech.thdev.samplepreference.repository 494 | 495 | import kotlinx.coroutines.flow.Flow 496 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 497 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 498 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 499 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 500 | 501 | @UsefulPreferences 502 | interface SecurityPreferences { 503 | 504 | @GetValue(KEY_INT) 505 | fun getInt(): Flow 506 | 507 | @GetValue(KEY_INT) 508 | suspend fun getIntValue(): Int 509 | 510 | @SetValue(KEY_INT) 511 | suspend fun setInt(value: Int) 512 | 513 | @GetValue(KEY_DOUBLE) 514 | fun getDouble(): Flow 515 | 516 | @SetValue(KEY_DOUBLE) 517 | suspend fun setDouble(value: Double) 518 | 519 | @GetValue(KEY_STRING) 520 | fun getString(): Flow 521 | 522 | @SetValue(KEY_STRING) 523 | suspend fun setString(value: String) 524 | 525 | @GetValue(KEY_BOOLEAN) 526 | fun getBoolean(): Flow 527 | 528 | @SetValue(KEY_BOOLEAN) 529 | suspend fun setBoolean(value: Boolean) 530 | 531 | @GetValue(KEY_FLOAT) 532 | fun getFloat(): Flow 533 | 534 | @SetValue(KEY_FLOAT) 535 | suspend fun setFloat(value: Float) 536 | 537 | @GetValue(KEY_LONG) 538 | fun getLong(): Flow 539 | 540 | @SetValue(KEY_LONG) 541 | suspend fun setLong(value: Long) 542 | 543 | @ClearValues 544 | suspend fun clearAll() 545 | 546 | companion object { 547 | 548 | private const val KEY_INT = "key-int" 549 | private const val KEY_DOUBLE = "key-double" 550 | private const val KEY_STRING = "key-string" 551 | private const val KEY_BOOLEAN = "key-boolean" 552 | private const val KEY_FLOAT = "key-float" 553 | private const val KEY_LONG = "key-long" 554 | } 555 | } 556 | """.trimIndent() 557 | ) 558 | 559 | val compilationResult = compile(securityClass, securityPreferences) 560 | 561 | // SecurityPreferencesKeys 562 | Assertions.assertEquals( 563 | """ 564 | package tech.thdev.samplepreference.repository 565 | 566 | import androidx.datastore.preferences.core.Preferences 567 | import androidx.datastore.preferences.core.Preferences.Key 568 | import androidx.datastore.preferences.core.stringPreferencesKey 569 | 570 | internal object SecurityPreferencesKeys { 571 | public val KEY_INT: Preferences.Key = stringPreferencesKey("key-int") 572 | 573 | public val KEY_DOUBLE: Preferences.Key = stringPreferencesKey("key-double") 574 | 575 | public val KEY_STRING: Preferences.Key = stringPreferencesKey("key-string") 576 | 577 | public val KEY_BOOLEAN: Preferences.Key = stringPreferencesKey("key-boolean") 578 | 579 | public val KEY_FLOAT: Preferences.Key = stringPreferencesKey("key-float") 580 | 581 | public val KEY_LONG: Preferences.Key = stringPreferencesKey("key-long") 582 | } 583 | """.trimIndent(), 584 | compilationResult.sourceFor("SecurityPreferencesKeys.kt").trimIndent().replaceTab() 585 | ) 586 | 587 | // SecurityPreferencesImpl 588 | Assertions.assertEquals( 589 | """ 590 | package tech.thdev.samplepreference.repository 591 | 592 | import androidx.datastore.core.DataStore 593 | import androidx.datastore.preferences.core.Preferences 594 | import androidx.datastore.preferences.core.edit 595 | import kotlinx.coroutines.flow.Flow 596 | import kotlinx.coroutines.flow.first 597 | import tech.thdev.useful.encrypted.`data`.store.preferences.extensions.editEncrypt 598 | import tech.thdev.useful.encrypted.`data`.store.preferences.extensions.mapDecrypt 599 | import tech.thdev.useful.encrypted.`data`.store.preferences.security.UsefulSecurity 600 | 601 | internal class SecurityPreferencesImpl( 602 | private val usefulSecurity: UsefulSecurity, 603 | private val preferencesStore: DataStore, 604 | ) : SecurityPreferences { 605 | public override fun getInt(): Flow = preferencesStore.data 606 | .mapDecrypt( 607 | usefulSecurity = usefulSecurity, 608 | type = Int::class, 609 | defaultValue = "0", 610 | ) { 611 | it[SecurityPreferencesKeys.KEY_INT] 612 | } 613 | 614 | public override suspend fun getIntValue(): Int = preferencesStore.data 615 | .mapDecrypt( 616 | usefulSecurity = usefulSecurity, 617 | type = Int::class, 618 | defaultValue = "0", 619 | ) { 620 | it[SecurityPreferencesKeys.KEY_INT] 621 | } 622 | .first() 623 | 624 | public override suspend fun setInt(`value`: Int) { 625 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 626 | preferences[SecurityPreferencesKeys.KEY_INT] = encrypted 627 | } 628 | } 629 | 630 | public override fun getDouble(): Flow = preferencesStore.data 631 | .mapDecrypt( 632 | usefulSecurity = usefulSecurity, 633 | type = Double::class, 634 | defaultValue = "0.0", 635 | ) { 636 | it[SecurityPreferencesKeys.KEY_DOUBLE] 637 | } 638 | 639 | public override suspend fun setDouble(`value`: Double) { 640 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 641 | preferences[SecurityPreferencesKeys.KEY_DOUBLE] = encrypted 642 | } 643 | } 644 | 645 | public override fun getString(): Flow = preferencesStore.data 646 | .mapDecrypt( 647 | usefulSecurity = usefulSecurity, 648 | type = String::class, 649 | defaultValue = "", 650 | ) { 651 | it[SecurityPreferencesKeys.KEY_STRING] 652 | } 653 | 654 | public override suspend fun setString(`value`: String) { 655 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 656 | preferences[SecurityPreferencesKeys.KEY_STRING] = encrypted 657 | } 658 | } 659 | 660 | public override fun getBoolean(): Flow = preferencesStore.data 661 | .mapDecrypt( 662 | usefulSecurity = usefulSecurity, 663 | type = Boolean::class, 664 | defaultValue = "false", 665 | ) { 666 | it[SecurityPreferencesKeys.KEY_BOOLEAN] 667 | } 668 | 669 | public override suspend fun setBoolean(`value`: Boolean) { 670 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 671 | preferences[SecurityPreferencesKeys.KEY_BOOLEAN] = encrypted 672 | } 673 | } 674 | 675 | public override fun getFloat(): Flow = preferencesStore.data 676 | .mapDecrypt( 677 | usefulSecurity = usefulSecurity, 678 | type = Float::class, 679 | defaultValue = "0.0F", 680 | ) { 681 | it[SecurityPreferencesKeys.KEY_FLOAT] 682 | } 683 | 684 | public override suspend fun setFloat(`value`: Float) { 685 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 686 | preferences[SecurityPreferencesKeys.KEY_FLOAT] = encrypted 687 | } 688 | } 689 | 690 | public override fun getLong(): Flow = preferencesStore.data 691 | .mapDecrypt( 692 | usefulSecurity = usefulSecurity, 693 | type = Long::class, 694 | defaultValue = "0", 695 | ) { 696 | it[SecurityPreferencesKeys.KEY_LONG] 697 | } 698 | 699 | public override suspend fun setLong(`value`: Long) { 700 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 701 | preferences[SecurityPreferencesKeys.KEY_LONG] = encrypted 702 | } 703 | } 704 | 705 | public override suspend fun clearAll() { 706 | preferencesStore.edit { 707 | it.clear() 708 | } 709 | } 710 | } 711 | 712 | public fun DataStore.generateSecurityPreferences(usefulSecurity: UsefulSecurity): 713 | SecurityPreferences = SecurityPreferencesImpl(preferencesStore = this, usefulSecurity = 714 | usefulSecurity) 715 | """.trimIndent(), 716 | compilationResult.sourceFor("SecurityPreferencesImpl.kt").trimIndent().replaceTab() 717 | ) 718 | 719 | Assertions.assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode) 720 | } 721 | 722 | @Test 723 | fun `test securityPreferences - default value`() { 724 | val securityClass = SourceFile.kotlin( 725 | "UsefulSecurity.kt", 726 | """ 727 | package tech.thdev.useful.encrypted.data.store.preferences.security 728 | 729 | interface UsefulSecurity { 730 | 731 | /** 732 | * Data to encryptData 733 | */ 734 | fun encryptData(data: String): String 735 | 736 | /** 737 | * encryptData to decrypt 738 | */ 739 | fun decryptData(encryptData: String): String 740 | } 741 | """.trimIndent() 742 | ) 743 | 744 | val securityPreferences = SourceFile.kotlin( 745 | "SecurityPreferences.kt", 746 | """ 747 | package tech.thdev.samplepreference.repository 748 | 749 | import kotlinx.coroutines.flow.Flow 750 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.UsefulPreferences 751 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.ClearValues 752 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.GetValue 753 | import tech.thdev.useful.encrypted.data.store.preferences.ksp.annotations.value.SetValue 754 | 755 | @UsefulPreferences 756 | interface SecurityPreferences { 757 | 758 | @GetValue(KEY_INT, "123") 759 | fun getInt(): Flow 760 | 761 | @GetValue(KEY_INT, "345") 762 | suspend fun getIntValue(): Int 763 | 764 | @SetValue(KEY_INT) 765 | suspend fun setInt(value: Int) 766 | 767 | @GetValue(KEY_DOUBLE, "122.1234") 768 | fun getDouble(): Flow 769 | 770 | @SetValue(KEY_DOUBLE) 771 | suspend fun setDouble(value: Double) 772 | 773 | @GetValue(KEY_STRING, "string value") 774 | fun getString(): Flow 775 | 776 | @SetValue(KEY_STRING) 777 | suspend fun setString(value: String) 778 | 779 | @GetValue(KEY_BOOLEAN, "true") 780 | fun getBoolean(): Flow 781 | 782 | @SetValue(KEY_BOOLEAN) 783 | suspend fun setBoolean(value: Boolean) 784 | 785 | @GetValue(KEY_FLOAT, "0.1234") 786 | fun getFloat(): Flow 787 | 788 | @SetValue(KEY_FLOAT) 789 | suspend fun setFloat(value: Float) 790 | 791 | @GetValue(KEY_LONG, "5151") 792 | fun getLong(): Flow 793 | 794 | @SetValue(KEY_LONG) 795 | suspend fun setLong(value: Long) 796 | 797 | @ClearValues 798 | suspend fun clearAll() 799 | 800 | companion object { 801 | 802 | private const val KEY_INT = "key-int" 803 | private const val KEY_DOUBLE = "key-double" 804 | private const val KEY_STRING = "key-string" 805 | private const val KEY_BOOLEAN = "key-boolean" 806 | private const val KEY_FLOAT = "key-float" 807 | private const val KEY_LONG = "key-long" 808 | } 809 | } 810 | """.trimIndent() 811 | ) 812 | 813 | val compilationResult = compile(securityClass, securityPreferences) 814 | 815 | // SecurityPreferencesKeys 816 | Assertions.assertEquals( 817 | """ 818 | package tech.thdev.samplepreference.repository 819 | 820 | import androidx.datastore.preferences.core.Preferences 821 | import androidx.datastore.preferences.core.Preferences.Key 822 | import androidx.datastore.preferences.core.stringPreferencesKey 823 | 824 | internal object SecurityPreferencesKeys { 825 | public val KEY_INT: Preferences.Key = stringPreferencesKey("key-int") 826 | 827 | public val KEY_DOUBLE: Preferences.Key = stringPreferencesKey("key-double") 828 | 829 | public val KEY_STRING: Preferences.Key = stringPreferencesKey("key-string") 830 | 831 | public val KEY_BOOLEAN: Preferences.Key = stringPreferencesKey("key-boolean") 832 | 833 | public val KEY_FLOAT: Preferences.Key = stringPreferencesKey("key-float") 834 | 835 | public val KEY_LONG: Preferences.Key = stringPreferencesKey("key-long") 836 | } 837 | """.trimIndent(), 838 | compilationResult.sourceFor("SecurityPreferencesKeys.kt").trimIndent().replaceTab() 839 | ) 840 | 841 | // SecurityPreferencesImpl 842 | Assertions.assertEquals( 843 | """ 844 | package tech.thdev.samplepreference.repository 845 | 846 | import androidx.datastore.core.DataStore 847 | import androidx.datastore.preferences.core.Preferences 848 | import androidx.datastore.preferences.core.edit 849 | import kotlinx.coroutines.flow.Flow 850 | import kotlinx.coroutines.flow.first 851 | import tech.thdev.useful.encrypted.`data`.store.preferences.extensions.editEncrypt 852 | import tech.thdev.useful.encrypted.`data`.store.preferences.extensions.mapDecrypt 853 | import tech.thdev.useful.encrypted.`data`.store.preferences.security.UsefulSecurity 854 | 855 | internal class SecurityPreferencesImpl( 856 | private val usefulSecurity: UsefulSecurity, 857 | private val preferencesStore: DataStore, 858 | ) : SecurityPreferences { 859 | public override fun getInt(): Flow = preferencesStore.data 860 | .mapDecrypt( 861 | usefulSecurity = usefulSecurity, 862 | type = Int::class, 863 | defaultValue = "123", 864 | ) { 865 | it[SecurityPreferencesKeys.KEY_INT] 866 | } 867 | 868 | public override suspend fun getIntValue(): Int = preferencesStore.data 869 | .mapDecrypt( 870 | usefulSecurity = usefulSecurity, 871 | type = Int::class, 872 | defaultValue = "345", 873 | ) { 874 | it[SecurityPreferencesKeys.KEY_INT] 875 | } 876 | .first() 877 | 878 | public override suspend fun setInt(`value`: Int) { 879 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 880 | preferences[SecurityPreferencesKeys.KEY_INT] = encrypted 881 | } 882 | } 883 | 884 | public override fun getDouble(): Flow = preferencesStore.data 885 | .mapDecrypt( 886 | usefulSecurity = usefulSecurity, 887 | type = Double::class, 888 | defaultValue = "122.1234", 889 | ) { 890 | it[SecurityPreferencesKeys.KEY_DOUBLE] 891 | } 892 | 893 | public override suspend fun setDouble(`value`: Double) { 894 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 895 | preferences[SecurityPreferencesKeys.KEY_DOUBLE] = encrypted 896 | } 897 | } 898 | 899 | public override fun getString(): Flow = preferencesStore.data 900 | .mapDecrypt( 901 | usefulSecurity = usefulSecurity, 902 | type = String::class, 903 | defaultValue = "string value", 904 | ) { 905 | it[SecurityPreferencesKeys.KEY_STRING] 906 | } 907 | 908 | public override suspend fun setString(`value`: String) { 909 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 910 | preferences[SecurityPreferencesKeys.KEY_STRING] = encrypted 911 | } 912 | } 913 | 914 | public override fun getBoolean(): Flow = preferencesStore.data 915 | .mapDecrypt( 916 | usefulSecurity = usefulSecurity, 917 | type = Boolean::class, 918 | defaultValue = "true", 919 | ) { 920 | it[SecurityPreferencesKeys.KEY_BOOLEAN] 921 | } 922 | 923 | public override suspend fun setBoolean(`value`: Boolean) { 924 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 925 | preferences[SecurityPreferencesKeys.KEY_BOOLEAN] = encrypted 926 | } 927 | } 928 | 929 | public override fun getFloat(): Flow = preferencesStore.data 930 | .mapDecrypt( 931 | usefulSecurity = usefulSecurity, 932 | type = Float::class, 933 | defaultValue = "0.1234", 934 | ) { 935 | it[SecurityPreferencesKeys.KEY_FLOAT] 936 | } 937 | 938 | public override suspend fun setFloat(`value`: Float) { 939 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 940 | preferences[SecurityPreferencesKeys.KEY_FLOAT] = encrypted 941 | } 942 | } 943 | 944 | public override fun getLong(): Flow = preferencesStore.data 945 | .mapDecrypt( 946 | usefulSecurity = usefulSecurity, 947 | type = Long::class, 948 | defaultValue = "5151", 949 | ) { 950 | it[SecurityPreferencesKeys.KEY_LONG] 951 | } 952 | 953 | public override suspend fun setLong(`value`: Long) { 954 | preferencesStore.editEncrypt(usefulSecurity, value) { preferences, encrypted -> 955 | preferences[SecurityPreferencesKeys.KEY_LONG] = encrypted 956 | } 957 | } 958 | 959 | public override suspend fun clearAll() { 960 | preferencesStore.edit { 961 | it.clear() 962 | } 963 | } 964 | } 965 | 966 | public fun DataStore.generateSecurityPreferences(usefulSecurity: UsefulSecurity): 967 | SecurityPreferences = SecurityPreferencesImpl(preferencesStore = this, usefulSecurity = 968 | usefulSecurity) 969 | """.trimIndent(), 970 | compilationResult.sourceFor("SecurityPreferencesImpl.kt").trimIndent().replaceTab() 971 | ) 972 | 973 | Assertions.assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode) 974 | } 975 | 976 | private fun String.replaceTab(): String = 977 | replace("\t", " ") 978 | 979 | private fun compile(vararg source: SourceFile) = KotlinCompilation().apply { 980 | sources = source.toList() 981 | symbolProcessorProviders = listOf(EncryptedDataStorePreferencesProcessorProvider()) 982 | workingDir = tempDir.toFile() 983 | inheritClassPath = true 984 | verbose = false 985 | messageOutputStream = System.out 986 | }.compile() 987 | 988 | companion object { 989 | @TempDir 990 | lateinit var tempDir: Path 991 | } 992 | 993 | private fun KotlinCompilation.Result.sourceFor(fileName: String): String { 994 | return kspGeneratedSources().find { it.name == fileName } 995 | ?.readText() 996 | ?: throw IllegalArgumentException("Could not find file $fileName in ${kspGeneratedSources()}") 997 | } 998 | 999 | private fun KotlinCompilation.Result.kspGeneratedSources(): List { 1000 | val kspWorkingDir = workingDir.resolve("ksp") 1001 | val kspGeneratedDir = kspWorkingDir.resolve("sources") 1002 | val kotlinGeneratedDir = kspGeneratedDir.resolve("kotlin") 1003 | val javaGeneratedDir = kspGeneratedDir.resolve("java") 1004 | return kotlinGeneratedDir.walk().toList() + 1005 | javaGeneratedDir.walk().toList() 1006 | } 1007 | 1008 | private val KotlinCompilation.Result.workingDir: File 1009 | get() = checkNotNull(outputDirectory.parentFile) 1010 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import tech.thdev.gradle.dependencies.Publish 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("lib-publish-android") 9 | } 10 | 11 | ext["libraryName"] = "useful-encrypted-data-store-preferences-security" 12 | ext["libraryVersion"] = libs.versions.libraryVersion.get() 13 | ext["description"] = Publish.description 14 | ext["url"] = Publish.publishUrl 15 | 16 | android { 17 | namespace = "tech.thdev.useful.encrypted.data.store.preferences.security" 18 | 19 | buildToolsVersion = libs.versions.buildToolsVersion.get() 20 | compileSdk = libs.versions.compileSdk.get().toInt() 21 | 22 | defaultConfig { 23 | minSdk = libs.versions.minSdk.get().toInt() 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("debug") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | 37 | getByName("release") { 38 | isMinifyEnabled = false 39 | proguardFiles( 40 | getDefaultProguardFile("proguard-android-optimize.txt"), 41 | "proguard-rules.pro" 42 | ) 43 | } 44 | } 45 | 46 | // AGP 8.0 47 | publishing { 48 | multipleVariants("release") { 49 | allVariants() 50 | } 51 | } 52 | 53 | compileOptions { 54 | sourceCompatibility = JavaVersion.VERSION_17 55 | targetCompatibility = JavaVersion.VERSION_17 56 | } 57 | 58 | kotlinOptions { 59 | jvmTarget = "17" 60 | } 61 | 62 | testOptions { 63 | unitTests { 64 | isIncludeAndroidResources = true 65 | } 66 | } 67 | } 68 | 69 | dependencies { 70 | implementation(libs.coroutines.core) 71 | implementation(libs.androidx.dataStorePreferencesCore) 72 | implementation(libs.androidx.securityCrypto) 73 | 74 | testImplementation(libs.test.androidx.core) 75 | testImplementation(libs.test.androidx.runner) 76 | testImplementation(libs.test.androidx.junit) 77 | testImplementation(libs.test.robolectric) 78 | testImplementation(libs.test.mockito.kotlin) 79 | testImplementation(libs.test.junit5) 80 | testImplementation(libs.test.junit5.engine) 81 | testRuntimeOnly(libs.test.junit5.vintage) 82 | testImplementation(libs.test.coroutines) 83 | testImplementation(libs.test.coroutines.turbine) 84 | 85 | testImplementation(libs.androidx.dataStorePreferences) 86 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taehwandev/EncryptedDataStorePreference/612db938fb751d7d3720558b762fa306ef4ce585/useful-encrypted-data-store-preferences-security/consumer-rules.pro -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/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.kts. 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 -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/extensions/UsefulSecurityDataStoreExtensions.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.extensions 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.MutablePreferences 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import kotlin.reflect.KClass 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import kotlinx.coroutines.flow.map 11 | import tech.thdev.useful.encrypted.data.store.preferences.security.UsefulSecurity 12 | 13 | suspend inline fun DataStore.editEncrypt( 14 | usefulSecurity: UsefulSecurity, 15 | value: Any, 16 | crossinline body: (preferences: MutablePreferences, encrypted: String) -> Unit 17 | ) { 18 | edit { preferences -> 19 | body(preferences, usefulSecurity.encryptData(value.toString())) 20 | } 21 | } 22 | 23 | inline fun Flow.mapDecrypt( 24 | usefulSecurity: UsefulSecurity, 25 | type: KClass<*>, 26 | defaultValue: String, 27 | crossinline body: (preferences: Preferences) -> String? 28 | ): Flow = 29 | map { preferences -> 30 | usefulSecurity.decryptData(body(preferences) ?: "") 31 | } 32 | .distinctUntilChanged() 33 | .map { 34 | @Suppress("UNCHECKED_CAST") 35 | when (type) { 36 | Int::class -> (it.takeIf { it.isNotEmpty() } ?: defaultValue).toInt() 37 | Double::class -> (it.takeIf { it.isNotEmpty() } ?: defaultValue).toDouble() 38 | String::class -> it.takeIf { it.isNotEmpty() } ?: defaultValue 39 | Boolean::class -> (it.takeIf { it.isNotEmpty() } ?: defaultValue).toBoolean() 40 | Float::class -> (it.takeIf { it.isNotEmpty() } ?: defaultValue).toFloat() 41 | Long::class -> (it.takeIf { it.isNotEmpty() } ?: defaultValue).toLong() 42 | else -> throw IllegalArgumentException("${type.simpleName} is not support type.") 43 | } as T 44 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/security/CipherWrapper.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security 2 | 3 | import android.util.Base64 4 | import androidx.annotation.VisibleForTesting 5 | import java.security.KeyStore 6 | import javax.crypto.Cipher 7 | import javax.crypto.spec.IvParameterSpec 8 | import tech.thdev.useful.encrypted.data.store.preferences.security.util.aesGenerateKey 9 | import tech.thdev.useful.encrypted.data.store.preferences.security.util.convertBase64 10 | import tech.thdev.useful.encrypted.data.store.preferences.security.util.convertSecretKey 11 | import tech.thdev.useful.encrypted.data.store.preferences.security.util.encode 12 | 13 | internal class CipherWrapper( 14 | private val keyEntry: KeyStore.Entry?, 15 | ) { 16 | 17 | @Synchronized 18 | fun encryptData(data: String): String { 19 | return if (keyEntry != null) { 20 | if (data.length > RSA_MAX_LENGTH) { 21 | encrypt(data) 22 | } else { 23 | try { 24 | encryptKeyStore(data) 25 | } catch (e: Exception) { 26 | // RSA key limit 27 | encrypt(data) 28 | } 29 | } 30 | } else { 31 | encrypt(data) 32 | } 33 | } 34 | 35 | private fun encrypt(data: String): String { 36 | val secretKey = aesGenerateKey() 37 | val cipher = Cipher.getInstance(CIPHER_OPTION).apply { 38 | init(Cipher.ENCRYPT_MODE, secretKey) 39 | } 40 | val final = cipher.doFinal(data.toByteArray()) 41 | 42 | return String.format("%s%s%s%s%s", secretKey.convertBase64(), DELIMITER, cipher.iv.encode(), DELIMITER, final.encode()) 43 | } 44 | 45 | private fun encryptKeyStore(data: String): String { 46 | val cipher = Cipher.getInstance(KEYSTORE_CIPHER_OPTION).apply { 47 | init(Cipher.ENCRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).certificate.publicKey) 48 | } 49 | val bytes = data.toByteArray(Charsets.UTF_8) 50 | val encryptedBytes = cipher.doFinal(bytes) 51 | 52 | return String.format("%s%s%s", encryptedBytes.encode(), DELIMITER, "end;") 53 | } 54 | 55 | fun decryptData(encryptData: String): String { 56 | if (encryptData.contains(DELIMITER).not()) { 57 | return encryptData 58 | } 59 | 60 | return if (keyEntry != null) { 61 | try { 62 | decrypt(encryptData) 63 | } catch (e: Exception) { 64 | try { 65 | decryptKeyStore(encryptData) 66 | } catch (e: InternalSecureException) { 67 | encryptData 68 | } catch (e: Exception) { 69 | "" 70 | } 71 | } 72 | } else { 73 | try { 74 | decrypt(encryptData) 75 | } catch (e: InternalSecureException) { 76 | encryptData 77 | } catch (e: Exception) { 78 | "" 79 | } 80 | } 81 | } 82 | 83 | private fun decrypt(encryptData: String): String { 84 | val target = encryptData.split(DELIMITER) 85 | if (target.size != 3) { 86 | throw InternalSecureException("Not support") 87 | } 88 | 89 | val secretKey = target[0].convertSecretKey() 90 | val encryptIv = Base64.decode(target[1], Base64.DEFAULT) 91 | val encryptTarget = Base64.decode(target[2], Base64.DEFAULT) 92 | 93 | val cipher = Cipher.getInstance(CIPHER_OPTION) 94 | cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(encryptIv)) 95 | 96 | return String(cipher.doFinal(encryptTarget)) 97 | } 98 | 99 | private fun decryptKeyStore(encryptData: String): String { 100 | val target = encryptData.split(DELIMITER) 101 | if (target.size != 2) { 102 | throw InternalSecureException("Not support") 103 | } 104 | 105 | val cipher = Cipher.getInstance(KEYSTORE_CIPHER_OPTION).apply { 106 | init(Cipher.DECRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).privateKey) 107 | } 108 | 109 | val encryptedBytes = Base64.decode(target[0], Base64.DEFAULT) 110 | val decryptedBytes = cipher.doFinal(encryptedBytes) 111 | 112 | return String(decryptedBytes) 113 | } 114 | 115 | companion object { 116 | 117 | private const val CIPHER_OPTION = "AES/CBC/PKCS5PADDING" 118 | 119 | @VisibleForTesting 120 | internal const val DELIMITER = "|" 121 | 122 | /** 123 | * AndroidKeystore 124 | */ 125 | private const val KEYSTORE_CIPHER_OPTION = "RSA/ECB/PKCS1Padding" 126 | 127 | private const val RSA_MAX_LENGTH = 42 128 | } 129 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/security/InternalSecureException.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security 2 | 3 | data class InternalSecureException(override val message: String?) : Exception(message) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/security/UsefulSecurity.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security 2 | 3 | interface UsefulSecurity { 4 | 5 | /** 6 | * Data to encryptData 7 | */ 8 | fun encryptData(data: String): String 9 | 10 | /** 11 | * encryptData to decrypt 12 | */ 13 | fun decryptData(encryptData: String): String 14 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/security/UsefulSecurityImpl.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security 2 | 3 | import android.security.keystore.KeyGenParameterSpec 4 | import android.security.keystore.KeyProperties 5 | import java.security.KeyPairGenerator 6 | import java.security.KeyStore 7 | import java.security.spec.RSAKeyGenParameterSpec 8 | 9 | 10 | internal class UsefulSecurityImpl( 11 | private val keyStoreAlias: String = DEFAULT_KEY_STORE_ALIAS, 12 | useTest: Boolean = false, 13 | ) : UsefulSecurity { 14 | 15 | private val keyStore = KeyStore.getInstance(PROVIDER_NAME).apply { 16 | load(null) 17 | } 18 | 19 | private val keyEntry: KeyStore.Entry? by lazy { 20 | if (useTest.not()) { 21 | val supportSecure = if (keyStore.containsAlias(keyStoreAlias)) { 22 | true 23 | } else { // KeyStore first time register 24 | kotlin.runCatching { 25 | KeyPairGenerator.getInstance( 26 | KeyProperties.KEY_ALGORITHM_RSA, 27 | PROVIDER_NAME, 28 | ).apply { 29 | initialize( 30 | KeyGenParameterSpec.Builder(keyStoreAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) 31 | .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(KEY_LENGTH_BIT, RSAKeyGenParameterSpec.F4)) 32 | .setBlockModes(KeyProperties.BLOCK_MODE_ECB) 33 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) 34 | .setDigests( 35 | KeyProperties.DIGEST_SHA512, 36 | KeyProperties.DIGEST_SHA384, 37 | KeyProperties.DIGEST_SHA256 38 | ) 39 | .setUserAuthenticationRequired(false) 40 | .build() 41 | ) 42 | }.generateKeyPair() 43 | true 44 | }.getOrDefault(false) 45 | } 46 | 47 | if (supportSecure) { 48 | keyStore.getEntry(keyStoreAlias, null) 49 | } else { 50 | null 51 | } 52 | } else { 53 | null 54 | } 55 | } 56 | 57 | @Synchronized 58 | override fun encryptData(data: String): String = 59 | CipherWrapper(keyEntry).encryptData(data) 60 | 61 | @Synchronized 62 | override fun decryptData(encryptData: String): String = 63 | CipherWrapper(keyEntry).decryptData(encryptData) 64 | 65 | companion object { 66 | 67 | private const val PROVIDER_NAME = "AndroidKeyStore" 68 | 69 | private const val KEY_LENGTH_BIT = 2048 70 | 71 | internal const val DEFAULT_KEY_STORE_ALIAS = "tech.thdev.useful.encrypted.data.store.preferences" 72 | } 73 | } 74 | 75 | fun generateUsefulSecurity( 76 | keyStoreAlias: String = UsefulSecurityImpl.DEFAULT_KEY_STORE_ALIAS, 77 | ): UsefulSecurity = 78 | UsefulSecurityImpl(keyStoreAlias = keyStoreAlias) -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/main/java/tech/thdev/useful/encrypted/data/store/preferences/security/util/UsefulSecurityUtil.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security.util 2 | 3 | import android.security.keystore.KeyProperties 4 | import android.util.Base64 5 | import java.io.ByteArrayInputStream 6 | import java.io.ByteArrayOutputStream 7 | import java.io.ObjectInputStream 8 | import java.io.ObjectOutputStream 9 | import javax.crypto.KeyGenerator 10 | import javax.crypto.SecretKey 11 | 12 | internal fun SecretKey.convertBase64(): String { 13 | val byteArray = ByteArrayOutputStream() 14 | val objectOutputStream = ObjectOutputStream(byteArray) 15 | objectOutputStream.writeObject(this) 16 | return byteArray.toByteArray().encode() 17 | } 18 | 19 | internal fun String.convertSecretKey(): SecretKey { 20 | val objectInputStream = ObjectInputStream(ByteArrayInputStream(this.decode())) 21 | return objectInputStream.readObject() as SecretKey 22 | } 23 | 24 | /** 25 | * Base64 Default convert 26 | */ 27 | internal fun ByteArray.encode(): String = 28 | Base64.encodeToString(this, Base64.DEFAULT) 29 | 30 | /** 31 | * String to ByteArray. Base64 Default 32 | */ 33 | internal fun String.decode(): ByteArray = 34 | Base64.decode(this, Base64.DEFAULT) 35 | 36 | internal fun aesGenerateKey(): SecretKey = 37 | KeyGenerator.getInstance( 38 | KeyProperties.KEY_ALGORITHM_AES, 39 | ).also { 40 | it.init(256) 41 | }.generateKey() -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/test/java/tech/thdev/useful/encrypted/data/store/preferences/extensions/MockDataStore.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.extensions 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import org.mockito.kotlin.mock 8 | 9 | internal class MockDataStore : DataStore { 10 | 11 | private val preferences = mock() 12 | 13 | private val sharedFlow = MutableStateFlow(preferences) 14 | 15 | override val data: Flow = 16 | sharedFlow 17 | 18 | override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { 19 | val value = transform(preferences) 20 | sharedFlow.value = value 21 | return value 22 | } 23 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/test/java/tech/thdev/useful/encrypted/data/store/preferences/extensions/UsefulSecurityDataStoreExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.extensions 2 | 3 | import android.os.Build 4 | import app.cash.turbine.test 5 | import kotlinx.coroutines.test.runTest 6 | import org.junit.Test 7 | import org.junit.jupiter.api.Assertions 8 | import org.junit.runner.RunWith 9 | import org.robolectric.RobolectricTestRunner 10 | import org.robolectric.annotation.Config 11 | import tech.thdev.useful.encrypted.data.store.preferences.security.UsefulSecurityImpl 12 | 13 | @Config(sdk = [Build.VERSION_CODES.S]) 14 | @RunWith(RobolectricTestRunner::class) 15 | internal class UsefulSecurityDataStoreExtensionsKtTest { 16 | 17 | private val security = UsefulSecurityImpl(useTest = true) 18 | private val datsStore = MockDataStore() 19 | 20 | @Test 21 | fun `test int - editEncrypt`() = runTest { 22 | val mockData = "1234511" 23 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 24 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 25 | } 26 | } 27 | 28 | @Test 29 | fun `test editEncrypt - Double`() = runTest { 30 | val mockData = "1234511.001" 31 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 32 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 33 | } 34 | } 35 | 36 | @Test 37 | fun `test editEncrypt - String`() = runTest { 38 | val mockData = "bbaddd" 39 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 40 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 41 | } 42 | } 43 | 44 | @Test 45 | fun `test editEncrypt - Boolean`() = runTest { 46 | val mockData = "true" 47 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 48 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 49 | } 50 | } 51 | 52 | @Test 53 | fun `test editEncrypt - Float`() = runTest { 54 | val mockData = "123f" 55 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 56 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 57 | } 58 | } 59 | 60 | @Test 61 | fun `test editEncrypt - Long`() = runTest { 62 | val mockData = "123L" 63 | datsStore.editEncrypt(security, mockData) { _, encrypted -> 64 | Assertions.assertEquals(mockData, security.decryptData(encrypted)) 65 | } 66 | } 67 | 68 | @Test 69 | fun `test mapDecrypt empty data - Int`() = runTest { 70 | val mockData = "" 71 | datsStore.data 72 | .mapDecrypt(security, Int::class, defaultValue = "0") { 73 | security.encryptData(mockData) 74 | } 75 | .test { 76 | Assertions.assertEquals(0, awaitItem()) 77 | cancelAndConsumeRemainingEvents() 78 | } 79 | } 80 | 81 | @Test 82 | fun `test mapDecrypt - Int`() = runTest { 83 | val mockData = "1000" 84 | datsStore.data 85 | .mapDecrypt(security, Int::class, defaultValue = "0") { 86 | security.encryptData(mockData) 87 | } 88 | .test { 89 | Assertions.assertEquals(mockData.toInt(), awaitItem()) 90 | cancelAndConsumeRemainingEvents() 91 | } 92 | } 93 | 94 | @Test 95 | fun `test mapDecrypt - Double`() = runTest { 96 | val mockData = "100.0" 97 | datsStore.data 98 | .mapDecrypt(security, Double::class, defaultValue = "0.0") { 99 | security.encryptData(mockData) 100 | } 101 | .test { 102 | Assertions.assertEquals(mockData.toDouble(), awaitItem()) 103 | cancelAndConsumeRemainingEvents() 104 | } 105 | } 106 | 107 | @Test 108 | fun `test mapDecrypt empty data - Double`() = runTest { 109 | val mockData = "" 110 | datsStore.data 111 | .mapDecrypt(security, Double::class, defaultValue = "0.0") { 112 | security.encryptData(mockData) 113 | } 114 | .test { 115 | Assertions.assertEquals(0.0, awaitItem()) 116 | cancelAndConsumeRemainingEvents() 117 | } 118 | } 119 | 120 | @Test 121 | fun `test mapDecrypt - String`() = runTest { 122 | val mockData = "aaa" 123 | datsStore.data 124 | .mapDecrypt(security, String::class, defaultValue = "") { 125 | security.encryptData(mockData) 126 | } 127 | .test { 128 | Assertions.assertEquals(mockData, awaitItem()) 129 | cancelAndConsumeRemainingEvents() 130 | } 131 | } 132 | 133 | @Test 134 | fun `test mapDecrypt empty data - string`() = runTest { 135 | val mockData = "" 136 | datsStore.data 137 | .mapDecrypt(security, String::class, defaultValue = "") { 138 | security.encryptData(mockData) 139 | } 140 | .test { 141 | Assertions.assertEquals("", awaitItem()) 142 | cancelAndConsumeRemainingEvents() 143 | } 144 | } 145 | 146 | @Test 147 | fun `test mapDecrypt - Boolean`() = runTest { 148 | val mockData = "true" 149 | datsStore.data 150 | .mapDecrypt(security, Boolean::class, defaultValue = "false") { 151 | security.encryptData(mockData) 152 | } 153 | .test { 154 | Assertions.assertEquals(mockData.toBoolean(), awaitItem()) 155 | cancelAndConsumeRemainingEvents() 156 | } 157 | } 158 | 159 | @Test 160 | fun `test mapDecrypt empty data - Boolean`() = runTest { 161 | val mockData = "" 162 | datsStore.data 163 | .mapDecrypt(security, Boolean::class, defaultValue = "false") { 164 | security.encryptData(mockData) 165 | } 166 | .test { 167 | Assertions.assertEquals(false, awaitItem()) 168 | cancelAndConsumeRemainingEvents() 169 | } 170 | } 171 | 172 | @Test 173 | fun `test mapDecrypt - Float`() = runTest { 174 | val mockData = "89090" 175 | datsStore.data 176 | .mapDecrypt(security, Float::class, defaultValue = "0.0") { 177 | security.encryptData(mockData) 178 | } 179 | .test { 180 | Assertions.assertEquals(mockData.toFloat(), awaitItem()) 181 | cancelAndConsumeRemainingEvents() 182 | } 183 | } 184 | 185 | @Test 186 | fun `test mapDecrypt empty data - Float`() = runTest { 187 | val mockData = "" 188 | datsStore.data 189 | .mapDecrypt(security, Float::class, defaultValue = "0.0") { 190 | security.encryptData(mockData) 191 | } 192 | .test { 193 | Assertions.assertEquals(0f, awaitItem()) 194 | cancelAndConsumeRemainingEvents() 195 | } 196 | } 197 | 198 | @Test 199 | fun `test mapDecrypt - Long`() = runTest { 200 | val mockData = "89090" 201 | datsStore.data 202 | .mapDecrypt(security, Long::class, defaultValue = "0") { 203 | security.encryptData(mockData) 204 | } 205 | .test { 206 | Assertions.assertEquals(mockData.toLong(), awaitItem()) 207 | cancelAndConsumeRemainingEvents() 208 | } 209 | } 210 | 211 | @Test 212 | fun `test mapDecrypt empty data - Long`() = runTest { 213 | val mockData = "" 214 | datsStore.data 215 | .mapDecrypt(security, Long::class, defaultValue = "0") { 216 | security.encryptData(mockData) 217 | } 218 | .test { 219 | Assertions.assertEquals(0L, awaitItem()) 220 | cancelAndConsumeRemainingEvents() 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /useful-encrypted-data-store-preferences-security/src/test/java/tech/thdev/useful/encrypted/data/store/preferences/security/UsefulSecurityImplTest.kt: -------------------------------------------------------------------------------- 1 | package tech.thdev.useful.encrypted.data.store.preferences.security 2 | 3 | import android.os.Build 4 | import org.junit.Test 5 | import org.junit.jupiter.api.Assertions 6 | import org.junit.runner.RunWith 7 | import org.robolectric.RobolectricTestRunner 8 | import org.robolectric.annotation.Config 9 | 10 | @Config(sdk = [Build.VERSION_CODES.S]) 11 | @RunWith(RobolectricTestRunner::class) 12 | internal class UsefulSecurityImplTest { 13 | 14 | private val security = UsefulSecurityImpl(useTest = true) 15 | 16 | @Test 17 | fun `test security`() { 18 | val originData = "origin-data" 19 | val encryptData = security.encryptData(originData) 20 | Assertions.assertEquals(3, encryptData.split(UsefulSecurityImpl.DELIMITER).size) 21 | val decryptData = security.decryptData(encryptData) 22 | Assertions.assertEquals(originData, decryptData) 23 | } 24 | } --------------------------------------------------------------------------------